Repository navigation
[material_ui] Add an opt-in keyboard focus ring for Material widgets - #13508
flutter-zl wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces FocusRingThemeData, FocusRingTheme, and ThemeData.focusRingTheme to support an opt-in Material 3 keyboard focus ring, alongside configuring component-specific offsets for buttons, chips, list tiles, tabs, and navigation destinations. Feedback suggests adding a guard in _strokeShape to handle non-positive dimensions from negative offsets, and optimizing FocusRingThemeData.lerp to avoid redundant object allocations when both arguments are null.
| void _strokeShape(Canvas canvas, Rect rect, double delta, Paint paint) { | ||
| final ShapeBorder? border = customBorder; |
There was a problem hiding this comment.
When drawing a focus ring with a negative offset (e.g., inside list tiles or tabs), the shape is deflated. If the widget's size is very small or animating to zero, the deflated rectangle can become inverted (where width or height is non-positive). This can cause rendering artifacts or assertions in RRect and canvas.drawRRect. Adding a guard at the start of _strokeShape to return early if the deflated dimensions are non-positive prevents these issues.
void _strokeShape(Canvas canvas, Rect rect, double delta, Paint paint) {
if (rect.width + 2 * delta <= 0 || rect.height + 2 * delta <= 0) {
return;
}
final ShapeBorder? border = customBorder;There was a problem hiding this comment.
Good catch. _strokeShape now skips bands whose rect would invert, with a test for a 4x4 widget.
| static FocusRingThemeData lerp(FocusRingThemeData? a, FocusRingThemeData? b, double t) { | ||
| if (identical(a, b) && a != null) { | ||
| return a; | ||
| } |
There was a problem hiding this comment.
When both a and b are null, identical(a, b) is true, but a != null is false, which causes the method to fall through and instantiate a new FocusRingThemeData with all null fields. We can optimize this by returning a ?? const FocusRingThemeData() directly when identical(a, b) is true, avoiding unnecessary object allocation.
static FocusRingThemeData lerp(FocusRingThemeData? a, FocusRingThemeData? b, double t) {
if (identical(a, b)) {
return a ?? const FocusRingThemeData();
}There was a problem hiding this comment.
Keeping this as is to match the other material_ui themes, such as BadgeThemeData.lerp. The null and null case already equals the const default.
Part of flutter/flutter#186044
Problem
Material widgets show keyboard focus only as a state layer, the content color at 10% opacity. That tint tops out near 1.3:1 against its background, well below the 3:1 that WCAG 1.4.11 requires of a focus indicator, so keyboard users cannot reliably see which control is focused. Material 3 specifies a ring-like keyboard focus indicator for this, defined per component by the
focus.indicatorcolor, thickness and offset tokens, but material_ui never reads those tokens.Fix
Adds
FocusRingThemeData,FocusRingThemeandThemeData.focusRingTheme. Whenvisibleis true,InkResponsedraws a ring around the keyboard-focused control, only inFocusHighlightMode.traditional, so touch and mouse are unaffected. The newInkResponse.focusRingOffsetplaces it per the M3 tokens: 2 px outside buttons and chips, 3 px inside list tiles, tabs and navigation destinations. The default ring is a 3 pxsecondaryband with anonSecondaryband inside it, because a singlesecondaryring measured 1.3:1 to 2:1 onprimaryapp bars and dark snackbars. It is off by default and adds nothing to the widget tree when off, so existing apps are unchanged. Checkbox, Radio, Switch and Slider paint focus separately and will follow.Demo
With
focusRingTheme: const FocusRingThemeData(visible: true)in a test app, I tabbed through 20 screens of buttons, chips, list tiles, tabs, navigation bars, rails and drawers, dialogs, the date picker and snackbars, and measured the contrast between each control focused and unfocused.Before: https://flutter-demo-90-before.web.app/#/text-button
After: https://flutter-demo-90-after.web.app/#/text-button
Press Tab to move focus. Other screens such as
#/navigation-bar,#/dialogand#/snack-barshow the same difference.