Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

O-Hannonen
Copy link
Contributor

This PR fixes #166398

  • Adds focus support for CupertinoActionSheetAction. This makes it work with keyboard shortcuts
  • Creates new widget, CupertinoTraversalGroup that applies a Cupertino style focus border around its child when any of its descendant has focus
  • Employs CupertinoTraversalGroup in CupertinoActionSheet

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

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository f: focus Focus traversal, gaining or losing focus labels Apr 14, 2025
…eyboard-focus-support-for-cupertino-action-sheet-action
@dkwingsmt dkwingsmt self-requested a review April 23, 2025 18:10
@dkwingsmt
Copy link
Contributor

It'll take me some time to review but first I want to say that this is a fascinating change!

Copy link
Contributor

@dkwingsmt dkwingsmt left a 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,
Copy link
Contributor

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.

Copy link
Contributor Author

@O-Hannonen O-Hannonen May 16, 2025

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

Copy link
Contributor Author

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

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 4, 2025

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.

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 4, 2025

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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.)

Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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(
Copy link
Contributor

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?

Copy link
Contributor Author

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:
image

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 CupertinoButtons.

image

Any thoughts on which approach would be better?

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 4, 2025

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. :)

Copy link
Contributor Author

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 4, 2025

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.

Copy link
Contributor Author

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]!;
Copy link
Contributor

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.

Copy link
Contributor Author

@O-Hannonen O-Hannonen May 16, 2025

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;
Copy link
Contributor

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 typedefs for examples.

Copy link
Contributor

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?

Copy link
Contributor Author

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);

Copy link
Contributor Author

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?

Copy link
Contributor

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!

@dkwingsmt
Copy link
Contributor

Gave another round of review. My apology that the review was a bit delayed due to the chaos around the US holiday lately.

@O-Hannonen O-Hannonen requested a review from dkwingsmt June 11, 2025 08:18
///
/// See also:
///
/// * <https://developer.apple.com/design/human-interface-guidelines/focus-and-selection/>
Copy link
Contributor

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(
Copy link
Contributor

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

Copy link
Contributor Author

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,
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: cupertino flutter/packages/flutter/cupertino repository f: focus Focus traversal, gaining or losing focus framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CupertinoActionSheet: Add keyboard support (Ipad, IOS)
3 participants