@@ -462,14 +462,16 @@ class _CupertinoAlertDialogState extends State<CupertinoAlertDialog> {
462462 width: isInAccessibilityMode
463463 ? _kAccessibilityCupertinoDialogWidth
464464 : _kCupertinoDialogWidth,
465- child: CupertinoPopupSurface (
466- isSurfacePainted: false ,
467- child: Semantics (
468- namesRoute: true ,
469- scopesRoute: true ,
470- explicitChildNodes: true ,
471- label: localizations.alertDialogLabel,
472- child: _buildBody (context),
465+ child: _ActionSheetGestureDetector (
466+ child: CupertinoPopupSurface (
467+ isSurfacePainted: false ,
468+ child: Semantics (
469+ namesRoute: true ,
470+ scopesRoute: true ,
471+ explicitChildNodes: true ,
472+ label: localizations.alertDialogLabel,
473+ child: _buildBody (context),
474+ ),
473475 ),
474476 ),
475477 ),
@@ -666,10 +668,10 @@ class _SlidingTapGestureRecognizer extends VerticalDragGestureRecognizer {
666668// Multiple `_SlideTarget`s might be nested.
667669// `_TargetSelectionGestureRecognizer` uses a simple algorithm that only
668670// compares if the inner-most slide target has changed (which suffices our use
669- // case). Semantically, this means that all outer targets will be treated as
670- // identical to the inner-most one, i.e. when the pointer enters or leaves a
671- // slide target, the corresponding method will be called on all targets that
672- // nest it.
671+ // case). Semantically, this means that all outer targets will be treated as
672+ // having the identical area as the inner-most one, i.e. when the pointer enters
673+ // or leaves a slide target, the corresponding method will be called on all
674+ // targets that nest it.
673675abstract class _SlideTarget {
674676 // A pointer has entered this region.
675677 //
@@ -682,7 +684,10 @@ abstract class _SlideTarget {
682684 //
683685 // The `fromPointerDown` should be true if this callback is triggered by a
684686 // PointerDownEvent, i.e. the second case from the list above.
685- void didEnter ({required bool fromPointerDown});
687+ //
688+ // The return value of this method is used as the `innerEnabled` for the next
689+ // target, while `innerEnabled` of the innermost target is true.
690+ bool didEnter ({required bool fromPointerDown, required bool innerEnabled});
686691
687692 // A pointer has exited this region.
688693 //
@@ -703,6 +708,10 @@ abstract class _SlideTarget {
703708
704709// Recognizes sliding taps and thereupon interacts with
705710// `_SlideTarget`s.
711+ //
712+ // TODO(dkwingsmt): It should recompute hit testing when the app is updated,
713+ // or better, share code with `MouseTracker`.
714+ // https://github.com/flutter/flutter/issues/155266
706715class _TargetSelectionGestureRecognizer extends GestureRecognizer {
707716 _TargetSelectionGestureRecognizer ({super .debugOwner, required this .hitTest})
708717 : _slidingTap = _SlidingTapGestureRecognizer (debugOwner: debugOwner) {
@@ -775,8 +784,12 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
775784 _currentTargets
776785 ..clear ()
777786 ..addAll (foundTargets);
787+ bool enabled = true ;
778788 for (final _SlideTarget target in _currentTargets) {
779- target.didEnter (fromPointerDown: fromPointerDown);
789+ enabled = target.didEnter (
790+ fromPointerDown: fromPointerDown,
791+ innerEnabled: enabled,
792+ );
780793 }
781794 }
782795 }
@@ -1234,7 +1247,9 @@ class _CupertinoActionSheetActionState extends State<CupertinoActionSheetAction>
12341247 implements _SlideTarget {
12351248 // |_SlideTarget|
12361249 @override
1237- void didEnter ({required bool fromPointerDown}) {}
1250+ bool didEnter ({required bool fromPointerDown, required bool innerEnabled}) {
1251+ return innerEnabled;
1252+ }
12381253
12391254 // |_SlideTarget|
12401255 @override
@@ -1377,11 +1392,15 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
13771392
13781393 // |_SlideTarget|
13791394 @override
1380- void didEnter ({required bool fromPointerDown}) {
1395+ bool didEnter ({required bool fromPointerDown, required bool innerEnabled}) {
1396+ // Action sheet doesn't support disabled buttons, therefore `innerEnabled`
1397+ // is always true.
1398+ assert (innerEnabled);
13811399 widget.onPressStateChange? .call (true );
13821400 if (! fromPointerDown) {
13831401 _emitVibration ();
13841402 }
1403+ return innerEnabled;
13851404 }
13861405
13871406 // |_SlideTarget|
@@ -1418,7 +1437,7 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
14181437 borderRadius: borderRadius,
14191438 ),
14201439 child: widget.child,
1421- )
1440+ ),
14221441 );
14231442 }
14241443}
@@ -1843,7 +1862,7 @@ class _CupertinoAlertActionSection extends StatelessWidget {
18431862
18441863// Renders the background of a button (both the pressed background and the idle
18451864// background) and reports its state to the parent with `onPressStateChange`.
1846- class _AlertDialogButtonBackground extends StatelessWidget {
1865+ class _AlertDialogButtonBackground extends StatefulWidget {
18471866 const _AlertDialogButtonBackground ({
18481867 required this .idleColor,
18491868 required this .pressedColor,
@@ -1868,37 +1887,58 @@ class _AlertDialogButtonBackground extends StatelessWidget {
18681887 /// Typically a [Text] widget.
18691888 final Widget child;
18701889
1871- void onTapDown (TapDownDetails details) {
1872- onPressStateChange? .call (true );
1890+ @override
1891+ _AlertDialogButtonBackgroundState createState () => _AlertDialogButtonBackgroundState ();
1892+ }
1893+
1894+ class _AlertDialogButtonBackgroundState extends State <_AlertDialogButtonBackground >
1895+ implements _SlideTarget {
1896+ void _emitVibration (){
1897+ switch (defaultTargetPlatform) {
1898+ case TargetPlatform .iOS:
1899+ case TargetPlatform .android:
1900+ HapticFeedback .selectionClick ();
1901+ case TargetPlatform .fuchsia:
1902+ case TargetPlatform .linux:
1903+ case TargetPlatform .macOS:
1904+ case TargetPlatform .windows:
1905+ break ;
1906+ }
1907+ }
1908+
1909+ // |_SlideTarget|
1910+ @override
1911+ bool didEnter ({required bool fromPointerDown, required bool innerEnabled}) {
1912+ widget.onPressStateChange? .call (innerEnabled);
1913+ if (innerEnabled && ! fromPointerDown) {
1914+ _emitVibration ();
1915+ }
1916+ return innerEnabled;
18731917 }
18741918
1875- void onTapUp (TapUpDetails details) {
1876- onPressStateChange? .call (false );
1919+ // |_SlideTarget|
1920+ @override
1921+ void didLeave () {
1922+ widget.onPressStateChange? .call (false );
18771923 }
18781924
1879- void onTapCancel () {
1880- onPressStateChange? .call (false );
1925+ // |_SlideTarget|
1926+ @override
1927+ void didConfirm () {
1928+ widget.onPressStateChange? .call (false );
18811929 }
18821930
18831931 @override
18841932 Widget build (BuildContext context) {
1885- final Color backgroundColor = pressed ? pressedColor : idleColor;
1886- return MergeSemantics (
1887- // TODO(mattcarroll): Button press dynamics need overhaul for iOS:
1888- // https://github.com/flutter/flutter/issues/19786
1889- child: GestureDetector (
1890- excludeFromSemantics: true ,
1891- behavior: HitTestBehavior .opaque,
1892- onTapDown: onTapDown,
1893- onTapUp: onTapUp,
1894- // TODO(mattcarroll): Cancel is currently triggered when user moves
1895- // past slop instead of off button: https://github.com/flutter/flutter/issues/19783
1896- onTapCancel: onTapCancel,
1933+ final Color backgroundColor = widget.pressed ? widget.pressedColor : widget.idleColor;
1934+ return MetaData (
1935+ metaData: this ,
1936+ child: MergeSemantics (
18971937 child: Container (
18981938 decoration: BoxDecoration (
18991939 color: CupertinoDynamicColor .resolve (backgroundColor, context),
19001940 ),
1901- child: child,
1941+ child: widget. child,
19021942 ),
19031943 ),
19041944 );
@@ -1911,7 +1951,7 @@ class _AlertDialogButtonBackground extends StatelessWidget {
19111951///
19121952/// * [CupertinoAlertDialog] , a dialog that informs the user about situations
19131953/// that require acknowledgment.
1914- class CupertinoDialogAction extends StatelessWidget {
1954+ class CupertinoDialogAction extends StatefulWidget {
19151955 /// Creates an action for an iOS-style dialog.
19161956 const CupertinoDialogAction ({
19171957 super .key,
@@ -1958,10 +1998,31 @@ class CupertinoDialogAction extends StatelessWidget {
19581998 /// Typically a [Text] widget.
19591999 final Widget child;
19602000
1961- /// Whether the button is enabled or disabled. Buttons are disabled by
1962- /// default. To enable a button, set its [onPressed] property to a non-null
1963- /// value.
1964- bool get enabled => onPressed != null ;
2001+ @override
2002+ State <CupertinoDialogAction > createState () => _CupertinoDialogActionState ();
2003+ }
2004+
2005+ class _CupertinoDialogActionState extends State <CupertinoDialogAction >
2006+ implements _SlideTarget {
2007+
2008+ // The button is enabled when it has [onPressed].
2009+ bool get enabled => widget.onPressed != null ;
2010+
2011+ // |_SlideTarget|
2012+ @override
2013+ bool didEnter ({required bool fromPointerDown, required bool innerEnabled}) {
2014+ return enabled;
2015+ }
2016+
2017+ // |_SlideTarget|
2018+ @override
2019+ void didLeave () {}
2020+
2021+ // |_SlideTarget|
2022+ @override
2023+ void didConfirm () {
2024+ widget.onPressed? .call ();
2025+ }
19652026
19662027 // Dialog action content shrinks to fit, up to a certain point, and if it still
19672028 // cannot fit at the minimum size, the text content is ellipsized.
@@ -1991,7 +2052,7 @@ class CupertinoDialogAction extends StatelessWidget {
19912052 ),
19922053 child: Semantics (
19932054 button: true ,
1994- onTap: onPressed,
2055+ onTap: widget. onPressed,
19952056 child: DefaultTextStyle (
19962057 style: textStyle,
19972058 textAlign: TextAlign .center,
@@ -2022,12 +2083,12 @@ class CupertinoDialogAction extends StatelessWidget {
20222083 Widget build (BuildContext context) {
20232084 TextStyle style = _kCupertinoDialogActionStyle.copyWith (
20242085 color: CupertinoDynamicColor .resolve (
2025- isDestructiveAction ? CupertinoColors .systemRed : CupertinoTheme .of (context).primaryColor,
2086+ widget. isDestructiveAction ? CupertinoColors .systemRed : CupertinoTheme .of (context).primaryColor,
20262087 context,
20272088 ),
2028- ).merge (textStyle);
2089+ ).merge (widget. textStyle);
20292090
2030- if (isDefaultAction) {
2091+ if (widget. isDefaultAction) {
20312092 style = style.copyWith (fontWeight: FontWeight .w600);
20322093 }
20332094
@@ -2047,20 +2108,19 @@ class CupertinoDialogAction extends StatelessWidget {
20472108 final Widget sizedContent = _isInAccessibilityMode (context)
20482109 ? _buildContentWithAccessibilitySizingPolicy (
20492110 textStyle: style,
2050- content: child,
2111+ content: widget. child,
20512112 )
20522113 : _buildContentWithRegularSizingPolicy (
20532114 context: context,
20542115 textStyle: style,
2055- content: child,
2116+ content: widget. child,
20562117 padding: padding,
20572118 );
20582119
20592120 return MouseRegion (
2060- cursor: onPressed != null && kIsWeb ? SystemMouseCursors .click : MouseCursor .defer,
2061- child: GestureDetector (
2062- excludeFromSemantics: true ,
2063- onTap: onPressed,
2121+ cursor: widget.onPressed != null && kIsWeb ? SystemMouseCursors .click : MouseCursor .defer,
2122+ child: MetaData (
2123+ metaData: this ,
20642124 behavior: HitTestBehavior .opaque,
20652125 child: ConstrainedBox (
20662126 constraints: const BoxConstraints (
0 commit comments