Skip to content

Can't apply TapRegion to a dropdown #128317

Open

Description

@rich-j

Is there an existing issue for this?

Use case

The TapRegion widget is great for detecting taps that occur either inside or outside a region. These regions are defined such that tapping on child widgets is considered to be inside the region of interest.

A child widget such as DropDownButton is only inside the region when tapped to activate. Once the dropdown is displayed any tap to either select or dismiss is considered as outside the region. Yes, the implementation of the dropdown is a popup with a barrier that are not rendered as children. Conceptually though, the dropdown is contained within the region and the entire behavior should be treated as inside.

Here is the DartPad "Counter Example" build method extended with a TapRegion and DropDownButton. Activating the dropdown is considered inside the region as can be seen in the console log. Tapping on the activated dropdown to select or dismiss is outside.

Code sample
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      // Any "taps" in any widget in the column is considered "inside" the region
      child: TapRegion(
        onTapInside: (_) => print("Tapped inside"),
        onTapOutside: (_) => print("Tapped OUTSIDE"),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // This button is "inside"
            IconButton(icon: const Icon(Icons.add), onPressed: _incrementCounter),
            // Activating the dropdown is "inside"
            // However all taps on the dropdown menu/barrier are "outside"
            DropdownButton(
                items: [1,2,3].map((n) => DropdownMenuItem(
                    value: n, child: Text("Item $n"))).toList(),
                onChanged: (c) => print("Dropdown pick $c")
            ),
          ],
        ),
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    ),
  );
}

TapRegion has a groupId parameter that allows various regions to be conceptually combined. However, DropDownButton provides no ability to access or augment the popup and barrier.

Proposal

Ideally, Flutter would recognize when widgets such as dropdowns and popups are contained in a TapRegion and extend that region to the resulting implementation that is not rendered as a child.

Alternatively, these widgets could expose some mechanism to allow the developer to augment the popup and barrier with a region groupId.

This request doesn't appear unprecedented. Flutter already includes a TextFieldTapRegion widget that extends the tap region for text editing.

Activity

  1. added
    in triagePresently being triaged by the triage team
    c: new featureNothing broken; request for a new capability
    frameworkflutter/packages/flutter repository. See also f: labels.
    p: material_uimaterial_ui package in flutter/packages
    c: proposalA detailed proposal for a change to Flutter
    f: gesturesflutter/packages/flutter/gestures repository.
    and removed
    p: material_uimaterial_ui package in flutter/packages
    on Jun 6, 2023
  2. removed
    in triagePresently being triaged by the triage team
    on Jun 19, 2023
  3. NicolasEymael commented on Jan 5, 2024

    @NicolasEymael

    any news on this?

  4. lewinpauli commented on Jun 18, 2024

    @lewinpauli
    Contributor

    any updates on this?

  5. flutter-triage-bot commented on Jul 8, 2024

    @flutter-triage-bot

    This issue is missing a priority label. Please set a priority label when adding the triaged-design label.

  6. added
    P2Important issues not at the top of the work list
    triaged-designTriaged by Design Languages team
    on Jul 11, 2024
  7. Piinks commented on Jul 11, 2024

    @Piinks
    Contributor

    FYI @gspencergoog is this intended behavior?

  8. gspencergoog commented on Jul 11, 2024

    @gspencergoog
    Contributor

    is this intended behavior?

    Yes, it's intended behavior for the TapRegion widget.

    But I'm not sure what the intended behavior should be for the dropdown widget. I think the dropdown is adding a modal barrier, which blocks any other events from getting a look in. The dropdown itself doesn't use a TapRegion.

    Ideally, Flutter would recognize when widgets such as dropdowns and popups are contained in a TapRegion and extend that region to the resulting implementation that is not rendered as a child.

    Implementing this as described would be hard to do, since there are a lot of edge cases that could occur (e.g. what if the dropdown is surrounded by two TapRegions? Do you duplicate both in the overlay/modal route?), and it feels like it's overly complicated.

    Also, what's a specific use case here? There already is a modal route for the dropdown, so it's not about the usual use for a TapRegion, which is getting the dropdown to go away when clicking somewhere else, since that will already happen.

    Could your desired result be obtained by wrapping the child of each item in a TapRegion with the same groupId as the one you put around the button?

  9. rich-j commented on Jul 12, 2024

    @rich-j
    Author

    Could your desired result be obtained by wrapping the child of each item in a TapRegion with the same groupId as the one you put around the button?

    Yes, except we're not aware of a supported way to get access to a popup/dropdown child/barrier to add a TapRegion groupId.

    We currently create an overlay with the TapRegion groupId that we need to explicitly open when these controls are activated and explicitly close when the controls are no longer active. However some native Flutter controls, such as DropdownButton, don't provide the necessary functionality - there is no way to know when the dropdown/barrier is active.

    Here are a few examples of what we can get working:

    Code sample

    The flutter PopupMenuButton provides the events onOpened and onCanceled that we use to create a full page overlay with the TapRegion.

    PopupMenuButton<String>(
        ...
        onSelected: (tag) {
          onAdd(tag);
          TapRegionX.closeOverlay();
        },
    
        /// When used in quick edit, the popup needs to be considered as
        /// belonging to the quick edit TapRegion. Extend the region to the cover the popup when active.
        onOpened: () => TapRegionX.openOverlay(context, QuickEdit.groupId),
        onCanceled: () => TapRegionX.closeOverlay(),
      );

    For the dropdown functionality, we use the user contributed DropdownButton2 that provides the event onMenuStateChange.

    DropdownButton2<String>(
        ...
        /// When this widget is in a TapRegion (e.g. quick edit) the groupId is provided
        /// that will extend the TapRegion into the dropdown and barrier (i.e. the entire screen)
        onMenuStateChange: (isOpen) {
          if (tapRegionGroupId != null) {
            if (isOpen)
              TapRegionX.openOverlay(context, tapRegionGroupId!);
            else
              TapRegionX.closeOverlay();
          }
        },
      );

    Here is our implementation of the overlay that includes the TapRegion:

    /// Support TapRegion being extended to cover widgets such as dropdown and popup.
    extension TapRegionX on TapRegion {
      /// The overlay entry must be remembered to be removed.
      /// Currently handle a single TapRegion extension with the state managed here.
      static OverlayEntry? _overlayEntry;
    
      /// Add a TapRegion overlay over the entire app to include the given groupId in the region.
      static void openOverlay(BuildContext context, Object groupId) {
        _overlayEntry?.remove();
        _overlayEntry = OverlayEntry(
          /// The overlay is a TapRegion that seems to expand over the entire app and passes on all taps.
          builder: (BuildContext context) =>
              TapRegion(groupId: groupId, behavior: HitTestBehavior.translucent, child: null),
        );
        Overlay.of(context, rootOverlay: true).insert(_overlayEntry!);
      }
    
      /// Remove a previously opened TapRegion overlay.
      static void closeOverlay() {
        _overlayEntry?.remove();
        _overlayEntry = null;
      }
    }

    One downside to the above implementation is there is a U/I flow that doesn't fire the expected events, so we need to brute force remove the overlay (sorry, I don't recall the details). As suggested by @gspencergoog, it would be better to wrap/augment the children, particularly the barrier, with the TapRegion groupId - this would avoid the need to create the overlay and would go away when the barrier does.

    We try to minimize third-party dependencies and would prefer to use the Flutter dropdown instead of the contributed DropdownButton2 that does provide needed events. The Flutter dropdown would need to provide notification of the popup opening and closing.

    As for one of our "use cases" (which you may call non-standard but works very well in our app and our users love it) we provide a "quick edit" mode that allows a few simple edits to list items. When the "quick edit" mode is entered we create a visual editing area with a TapRegion that allows changes to be made to a few values and when the area is exited (tapping outside or focus tabbing) the area is closed and changes committed. Text fields, radio buttons, checkboxes all work well. Our problem are popup/dropdown, it's visually and conceptually in the TapRegion, however when activated the resulting barrier/popup is not.

    The "ideal" solution is that a popup/dropdown just works when the base control is in one or many TapRegions - the resulting popup/barrier would inherit the region(s). However, we recognize that TapRegion use is somewhat specialized and that the developer can add the region to the popup/barrier if Flutter native controls would provide the means to do so.

  10. gspencergoog commented on Jul 12, 2024

    @gspencergoog
    Contributor

    The Flutter dropdown would need to provide notification of the popup opening and closing.

    So, would adding a callback on closing the popup solve your issue? If it's that simple, I don't see a problem with doing that.

    As for one of our "use cases" (which you may call non-standard but works very well in our app and our users love it) we provide a "quick edit" mode that allows a few simple edits to list items.

    If your users want it, and it's useful, then it doesn't matter if it's non-standard or not: it is still a valid use case.

    The "ideal" solution is that a popup/dropdown just works when the base control is in one or many TapRegions - the resulting popup/barrier would inherit the region(s).

    Yes, I can see that would help your implementation. I wonder if the right solution to this would be to convert the implementation of the DropdownButton to use OverlayPortal instead of the Overlay directly. That would give the illusion to the widgets in the menu, including the panel, that they were descendants of the button in the widget tree, and so also descendants of the TapRegion you put around it. The down side of that is that it would require a pretty huge change to all the PopupRoute dependent things in the SDK. This would be a lot of work, so I wouldn't count on it any time soon, but we could file an issue for that. It might be a good change to make in general, for a number of other use cases as well.

  11. gspencergoog commented on Jul 12, 2024

    @gspencergoog
    Contributor

    I filed #151671 as a proposal for the long term solution. As I said, though, that might take quite a while.

  12. rich-j commented on Jul 25, 2024

    @rich-j
    Author

    So, would adding a callback on closing the popup solve your issue? If it's that simple, I don't see a problem with doing that.

    Yes, knowing when the dropdown popup opens (already available) and closes would allow us to use the Flutter dropdown instead of a contributed one. When the dropdown is "active" with a popup, a click anywhere dismisses the popup - thus we create a full screen overlay that extends the TapRegion to make such click as in-region. This overlay must be removed when the popup is dismissed - thus the need for a close callback.

    I filed #151671 as a proposal for the long term solution.

    Thank you. This is the preferred solution - widgets (e.g. dropdown) that temporarily extend beyond a TapRegion during user interaction should still be considered as "in-region".

  13. soufianebenyaala commented on Nov 12, 2024

    @soufianebenyaala
  14. added
    packageflutter/packages repository. See also p: labels.
    and removed
    frameworkflutter/packages/flutter repository. See also f: labels.
    on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterf: gesturesflutter/packages/flutter/gestures repository.p: material_uimaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.team-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions