-
Notifications
You must be signed in to change notification settings - Fork 28.8k
Add focus support for CupertinoActionSheetAction #166398 #167119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add focus support for CupertinoActionSheetAction #166398 #167119
Conversation
…upertinoTraversalGroup
…eyboard-focus-support-for-cupertino-action-sheet-action
It'll take me some time to review but first I want to say that this is a fascinating change! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A first round of comments.
@@ -1077,6 +1080,7 @@ class CupertinoActionSheet extends StatefulWidget { | |||
this.messageScrollController, | |||
this.actionScrollController, | |||
this.cancelButton, | |||
this.focusColor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend removing customization for focusColor
for the following reasons:
- A single color might not be sufficient to describe the border. The video for the native case displayed two borders with two colors. I don't mean that this PR must reach 100% fidelity, since it's already a great start, but describing the border with a single color is in the end not correct and will be problematic when we try to improve the fidelity.
- This feature is a system wide accessibility feature and probably shouldn't need customization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points and I agree! The system wide settings actually can be customized from iPhone settings (the color can be changed to something different than the default blue). But not sure if we even can access that customization in Flutter.
This focusColor
was highly inspired on how eg. CupertinoButton
and CupertinoCheckbox
does it. They both accept a single focusColor
parameter. Not 100% sure what colors the native counterpart uses for the two color borders, but would assume the second color is something that is calculated based on the first color (added opacity or luminance) -> in theory the single color could still be enough to improve the fidelity.
I'd say it should be enough to have it hardcoded to the blue as its the system wide default color and we probably can't access the info of which color the user has possibly customized it to. But if there is a need to customize the color within an app, IMO it should be an app-wide setting (eg. that the color reflects to changes in CupertinoThemeData
). That being said, I'll remove the focusColor
from this widget for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also arises the question that should some of the widgets that already do similar halo internally take this new widget into use? Like CupertinoButton
and CupertinoCheckbox
? Maybe anyways out of the scope of this PR but something to consider
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also arises the question that should some of the widgets that already do similar halo internally take this new widget into use? Like CupertinoButton and CupertinoCheckbox ? Maybe anyways out of the scope of this PR but something to consider
I would definitely like to see the design being consistent across Cupertino widgets. Do you mean that there are already widgets having this focus halo? Or do you mean the focus background color?
I think it's an interesting question indeed whether we should keep the focus background color. We probably added it unaware of the halo, and we can always correct it. The problem is mostly the behavior when Cupertino widgets are run on non-Apple platforms. We can probably discuss it in a separate github issue. In general, I'd expect that there's a consistent style across Cupertino widgets when running on a given platform.
(My gut tells me that it should somehow depend on the targetPlatform
and/or be globally configurable, but I'm not strong on it.)
As of this PR, I think the changes in this PR is safe, mostly because 1) it improves the widget toward the official behavior, and 2) it doesn't add new APIs to the widget, so we can always improve in the future without worrying about anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh actually I found this WWDC video https://developer.apple.com/videos/play/wwdc2021/10260/ that introduces the halo API. It seems that Apple support two kind of focus effects: some widgets have the focus halo and some widgets only have the focus highlight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah some use halo and some use background color to highlight the focus. But from existing Flutter Cupertino widgets, some already have a similar halo in place (eg. CupertinoCheckbox or CupertinoButton). So on some other issue/PR could be nice to unify those to use the new halo widget internally too.
/// | ||
/// * <https://developer.apple.com/design/human-interface-guidelines/focus-and-selection/> | ||
/// {@endtemplate} | ||
class CupertinoFocusTraversalGroup extends StatefulWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I noticed that this effect is called UIFocusHaloEffect
in iOS's API and referred to as a "focus ring" in Apple's guide. I think either one would be a great simple name: CupertinoHalo
, CupertinoFocusHalo
or CupertinoFocusRing
.
(We don't have to change it right now. Unless you have a clear opinion, we can postpone this decision while I ask round for opinions.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm that's true, maybe the initially proposed names were stuck a bit too much on the traversal. Maybe the right name should come from the terminology Apple uses.
To me CupertinoFocusHalo
would sound the best out of these options. IMO its good that it still talks about the focus and as discussed in the other comments its not always a "ring", it can also be square if the usage specifies so. But let me know when you've gathered some opinions!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the opinion! That sounds pretty nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rename the widget this way? This a great name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, renamed!
context, | ||
const BorderRadius borderRadius = BorderRadius.all(Radius.circular(_kCornerRadius)); | ||
|
||
child = ClipRRect( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this clip for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its because CupertinoActionSheetAction
draws the background itself when focused and that would overflow, like in this picture:
This is only necessary for the cancel button as _ActionSheetMainSheet
is already wrapped with ClipRSuperellipse
.
Another option would also be that CupertinoActionSheetAction
does not draw the focus background itself but this _ActionSheetButtonBackground
would draw it. But as the actions can be any widget, that may lead to very weird looking UIs if actions are something other than CupertinoActionSheetAction
, eg if the actions are CupertinoButton
s.

Any thoughts on which approach would be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it work if you configure the focus color at this line? (L1678)
widget.pressed ? _kActionSheetCancelPressedColor : _kActionSheetCancelColor,
If not and we have to use clip, then you can remove the entire ShapeDecoration
part and move the color to DecoratedBox
's background color. The ShapeDecoration
is to draw the button shape, which is no longer necessary with the clipping.
Also, can you should replace ClipRRect
with ClipRSuperellipse
? This is the new shape that corresponds to the continuous border RRect. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it work if you configure the focus color at this line? (L1678)
We could do that and it would help to get rid of the clipping. But since actions
on CupertinoActionSheet
can be any widget, that would lead to weird UIs (eg. if action is CupertinoButton like in the picture above).
IMO its cleaner if the widgets that are passed as actions render their own focus highlight internally, so implemented now the suggestions on improving the clipping and decoration instead of removing it. Does that sound reasonable?
class CupertinoFocusTraversalGroup extends StatefulWidget { | ||
/// {@macro flutter.cupertino.CupertinoFocusTraversalGroup} | ||
const CupertinoFocusTraversalGroup({ | ||
this.borderRadius, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The borderRadius
is very limiting. The API used by iOS has different initializer for different shapes, which I think is a good design. I suggest using named constructors, such as CupertinoFocusTraversalGroup.onRRect
CupertinoFocusTraversalGroup.onRect
to leave room for other shapes in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, added named constructors!
/// The radius of the border that highlights active focus. | ||
/// | ||
/// When [borderRadius] is null, it defaults to [CupertinoFocusTraversalGroup.defaultBorderRadius] | ||
final BorderRadiusGeometry? borderRadius; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave this property private so that this class can support other shapes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So CupertinoFocusTraversalGroup.onRRect
would accept some other type of parameter than BorderRadiusGeometry
and assign the private _borderRadius
based on that? What type would that be then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CupertinoFocusTraversalGroup.onRRect
will receive a BorderRadiusGeometry? borderRadius
but the border radius will not be accessible as a property (basically renaming it to BorderRadiusGeometry? _borderRadius
). For example, if there is another constructors called CupertinoFocusTraversalGroup.onShape
that receives a ShapeBorder shape
, how would the borderRadius
getter work? Since it's a widget, its properties wouldn't be retrieved anyway, therefore I suggest hiding properties related to shapes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, now I get it. Thanks for the explanation! Changed the border radius to private now
|
||
/// The default radius of the border that highlights active focus. | ||
static BorderRadius get defaultBorderRadius => | ||
kCupertinoButtonSizeBorderRadius[CupertinoButtonSize.large]!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't make much sense to me, since the border radius should follow exactly the child widget and should be provided explicitly. If there must be any default, then the default should be a rectangle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, default is removed.
/// | ||
/// Called with true if any node within the group has focus, and false | ||
/// otherwise. | ||
final ValueChanged<bool>? onFocusChange; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By Flutter's style, functional types must be typedef
'd before being used. Search for other typedef
s for examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also: I'm not an expert on focus, but is this really not achievable with existing APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValueChanged
is already a typedef
So do you mean that we should create a typedef for the typedef?
typedef FocusChanged = ValueChanged<bool>;
Or that we should not use the ValueChanged
?
typedef FocusChanged = void Function(bool value);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK this is not doable with existing APIs. If we would have access to focus nodes we could listen to those but in this case we don't have.
But maybe you could request review from someone who has worked a lot with focus?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValueChanged is already a typedef
Ah my bad. You're right.
But maybe you could request review from someone who has worked a lot with focus?
Yeah I definitely will!
Gave another round of review. My apology that the review was a bit delayed due to the chaos around the US holiday lately. |
/// | ||
/// See also: | ||
/// | ||
/// * <https://developer.apple.com/design/human-interface-guidelines/focus-and-selection/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider adding an example for this widget
|
||
@override | ||
Widget build(BuildContext context) { | ||
return FocusTraversalGroup( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a FocusTraversalGroup? I think a Focus(canRequestFocus: false, skipTraversal: true, includeSemantics: false)
should do the job
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that would probably do the trick for now, but then we lose the possibility to do FocusTraversalGroup.of(context)
lookups for FocusTraversalPolicy
. Would that be a problem in the future? I would assume some Cupertino widgets (eg. context menu) will benefit from that once the traversal support is added for them
@@ -2039,6 +2039,7 @@ class FocusTraversalGroup extends StatefulWidget { | |||
FocusTraversalPolicy? policy, | |||
this.descendantsAreFocusable = true, | |||
this.descendantsAreTraversable = true, | |||
this.onFocusChange, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't needed if we use Focus widget directly
This PR fixes #166398
CupertinoActionSheetAction
. This makes it work with keyboard shortcutsCupertinoTraversalGroup
that applies a Cupertino style focus border around its child when any of its descendant has focusCupertinoTraversalGroup
inCupertinoActionSheet
How the new implementation looks and behaves:
https://github.com/user-attachments/assets/ea6789f1-921d-4598-bcca-489dc063ff73
How the native counterpart looks and behaves:
https://github.com/user-attachments/assets/4c6ae2a0-7205-4de2-b981-ec7f4839da6e
Pre-launch Checklist
///
).