Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/dropdown_button2/lib/src/dropdown_button2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@

import 'dart:math' as math;
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'seperated_sliver_child_builder_delegate.dart';

part 'button_style_data.dart';
part 'dropdown_style_data.dart';
part 'dropdown_route.dart';
part 'dropdown_menu.dart';
part 'dropdown_menu_item.dart';
part 'dropdown_menu_separators.dart';
part 'dropdown_route.dart';
part 'dropdown_style_data.dart';
part 'enums.dart';
part 'utils.dart';

Expand Down Expand Up @@ -123,6 +125,7 @@ class DropdownButton2<T> extends StatefulWidget {
this.openWithLongPress = false,
this.barrierDismissible = true,
this.barrierCoversButton = true,
this.barrierBlocksInteraction = true,
this.barrierColor,
this.barrierLabel,
this.openDropdownListenable,
Expand Down Expand Up @@ -163,6 +166,7 @@ class DropdownButton2<T> extends StatefulWidget {
required this.openWithLongPress,
required this.barrierDismissible,
required this.barrierCoversButton,
required this.barrierBlocksInteraction,
required this.barrierColor,
required this.barrierLabel,
required this.openDropdownListenable,
Expand Down Expand Up @@ -352,6 +356,14 @@ class DropdownButton2<T> extends StatefulWidget {
/// Defaults to true.
final bool barrierCoversButton;

/// Whether to block interaction with underlying widgets when the dropdown is open.
///
/// When false, taps outside the dropdown can pass through to underlying
/// widgets while still dismissing the dropdown.
///
/// Defaults to true.
final bool barrierBlocksInteraction;

/// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent.
final Color? barrierColor;
Expand Down Expand Up @@ -698,6 +710,7 @@ class _DropdownButton2State<T> extends State<DropdownButton2<T>> with WidgetsBin
barrierLabel:
widget.barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierCoversButton: widget.barrierCoversButton,
barrierBlocksInteraction: widget.barrierBlocksInteraction,
parentFocusNode: _focusNode,
enableFeedback: widget.enableFeedback ?? true,
textDirection: textDirection,
Expand Down Expand Up @@ -1090,6 +1103,7 @@ class DropdownButtonFormField2<T> extends FormField<T> {
bool openWithLongPress = false,
bool barrierDismissible = true,
bool barrierCoversButton = true,
bool barrierBlocksInteraction = true,
Color? barrierColor,
String? barrierLabel,
Listenable? openDropdownListenable,
Expand Down Expand Up @@ -1181,6 +1195,7 @@ class DropdownButtonFormField2<T> extends FormField<T> {
openWithLongPress: openWithLongPress,
barrierDismissible: barrierDismissible,
barrierCoversButton: barrierCoversButton,
barrierBlocksInteraction: barrierBlocksInteraction,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
openDropdownListenable: openDropdownListenable,
Expand Down
177 changes: 140 additions & 37 deletions packages/dropdown_button2/lib/src/dropdown_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
Color? barrierColor,
this.barrierLabel,
required this.barrierCoversButton,
required this.barrierBlocksInteraction,
required this.parentFocusNode,
required this.enableFeedback,
required this.textDirection,
Expand Down Expand Up @@ -59,12 +60,22 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {

final bool barrierCoversButton;

final bool barrierBlocksInteraction;

final FocusScopeNode _childNode = FocusScopeNode(debugLabel: 'Child');

@override
Widget buildModalBarrier() => IgnorePointer(
ignoring: !barrierBlocksInteraction,
child: super.buildModalBarrier(),
);

@override
Widget buildPage(BuildContext context, _, __) {
final TextDirection resolvedTextDirection =
textDirection ?? Directionality.of(context);
return Directionality(
textDirection: textDirection ?? Directionality.of(context),
textDirection: resolvedTextDirection,
child: FocusScope.withExternalFocusNode(
focusScopeNode: _childNode,
parentNode: parentFocusNode,
Expand Down Expand Up @@ -94,15 +105,56 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
style: style,
enableFeedback: enableFeedback,
);
return barrierCoversButton
final dismissibleRoutePage = barrierBlocksInteraction
? routePage
: Stack(
fit: StackFit.expand,
children: [
// In non-blocking mode, listen for outside taps so we can
// dismiss the menu, while still passing the same pointer
// event through to underlying widgets.
if (barrierDismissible)
Positioned.fill(
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (PointerDownEvent event) {
final Rect menuRect = getMenuRect(
buttonRect: rect,
availableSize: Size(
constraints.maxWidth,
actualConstraints.maxHeight,
),
mediaQueryPadding: mediaQueryPadding,
textDirection: resolvedTextDirection,
);
if (!menuRect.contains(event.position)) {
_dismiss();
}
},
),
),
routePage,
// Always intercept anchor-button taps while the menu is open
// so the underlying button does not receive the same gesture.
// If the route is dismissible, treat the tap as close.
Positioned.fromRect(
rect: rect,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: barrierDismissible ? _dismiss : () {},
),
),
],
);
return barrierCoversButton
? dismissibleRoutePage
: _CustomModalBarrier(
barrierColor: _altBarrierColor,
animation: animation,
barrierCurve: barrierCurve,
buttonRect: rect,
buttonBorderRadius: buttonBorderRadius ?? BorderRadius.zero,
child: routePage,
child: dismissibleRoutePage,
);
},
);
Expand Down Expand Up @@ -242,6 +294,81 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
}

double getMenuWidth({
required double availableWidth,
required double buttonWidth,
}) {
// The width of a menu should be at most the view width. This ensures that
// the menu does not extend past the left and right edges of the screen.
final double? configuredMenuWidth = dropdownStyle.width;
return math.min(availableWidth, configuredMenuWidth ?? buttonWidth);
}

double getMenuLeft({
required Rect buttonRect,
required double availableWidth,
required double menuWidth,
required TextDirection textDirection,
}) {
final Offset offset = dropdownStyle.offset;
return switch (dropdownStyle.direction) {
DropdownDirection.textDirection => switch (textDirection) {
TextDirection.rtl => clampDouble(
buttonRect.right - menuWidth + offset.dx,
0.0,
availableWidth - menuWidth,
),
TextDirection.ltr => clampDouble(
buttonRect.left + offset.dx,
0.0,
availableWidth - menuWidth,
),
},
DropdownDirection.right => clampDouble(
buttonRect.left + offset.dx,
0.0,
availableWidth - menuWidth,
),
DropdownDirection.left => clampDouble(
buttonRect.right - menuWidth + offset.dx,
0.0,
availableWidth - menuWidth,
),
DropdownDirection.center => clampDouble(
(availableWidth - menuWidth) / 2 + offset.dx,
0.0,
availableWidth - menuWidth,
),
};
}

Rect getMenuRect({
required Rect buttonRect,
required Size availableSize,
required EdgeInsets mediaQueryPadding,
required TextDirection textDirection,
}) {
final _MenuLimits menuLimits = getMenuLimits(
buttonRect,
availableSize.height,
mediaQueryPadding,
selectedIndex,
);

final double menuWidth = getMenuWidth(
availableWidth: availableSize.width,
buttonWidth: buttonRect.width,
);
final double left = getMenuLeft(
buttonRect: buttonRect,
availableWidth: availableSize.width,
menuWidth: menuWidth,
textDirection: textDirection,
);

return Rect.fromLTWH(left, menuLimits.top, menuWidth, menuLimits.height);
}

// The maximum height of a simple menu should be one or more rows less than
// the view height. This ensures a tappable area outside of the simple menu
// with which to dismiss the menu.
Expand Down Expand Up @@ -373,10 +500,10 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
if (preferredMaxHeight != null && preferredMaxHeight <= maxHeight) {
maxHeight = preferredMaxHeight;
}
// The width of a menu should be at most the view width. This ensures that
// the menu does not extend past the left and right edges of the screen.
final double? menuWidth = route.dropdownStyle.width;
final double width = math.min(constraints.maxWidth, menuWidth ?? buttonRect.width);
final double width = route.getMenuWidth(
availableWidth: constraints.maxWidth,
buttonWidth: buttonRect.width,
);
return BoxConstraints(
minWidth: width,
maxWidth: width,
Expand Down Expand Up @@ -406,36 +533,12 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
}());
assert(textDirection != null);

final Offset offset = route.dropdownStyle.offset;
final double left = switch (route.dropdownStyle.direction) {
DropdownDirection.textDirection => switch (textDirection!) {
TextDirection.rtl => clampDouble(
buttonRect.right - childSize.width + offset.dx,
0.0,
size.width - childSize.width,
),
TextDirection.ltr => clampDouble(
buttonRect.left + offset.dx,
0.0,
size.width - childSize.width,
),
},
DropdownDirection.right => clampDouble(
buttonRect.left + offset.dx,
0.0,
size.width - childSize.width,
),
DropdownDirection.left => clampDouble(
buttonRect.right - childSize.width + offset.dx,
0.0,
size.width - childSize.width,
),
DropdownDirection.center => clampDouble(
(size.width - childSize.width) / 2 + offset.dx,
0.0,
size.width - childSize.width,
),
};
final double left = route.getMenuLeft(
buttonRect: buttonRect,
availableWidth: size.width,
menuWidth: childSize.width,
textDirection: textDirection!,
);

return Offset(left, menuLimits.top);
}
Expand Down
Loading