diff --git a/CHANGELOG.md b/CHANGELOG.md index d142e3b..a949dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed + +- `WAnchor` now hit-tests its whole bounds via `HitTestBehavior.translucent`. The inner `GestureDetector` used the default `HitTestBehavior.deferToChild`, so it only fired when a painted child sat under the exact tap point. An anchor wrapping transparent content (a settings row, a checkbox row, a link with padding) ignored taps that landed on its empty regions, including the element centre that automated drivers and centred pointer events target. It now behaves like `WInput`/`WPopover`, which already use whole-box hit-testing, so the full anchor rectangle is tappable. (`lib/src/widgets/w_anchor.dart`; covered by `test/widgets/w_anchor_test.dart`) + ## [1.2.0] - 2026-07-08 ### Changed diff --git a/lib/src/widgets/w_anchor.dart b/lib/src/widgets/w_anchor.dart index a53cb2c..1db7f15 100644 --- a/lib/src/widgets/w_anchor.dart +++ b/lib/src/widgets/w_anchor.dart @@ -198,6 +198,16 @@ class _WAnchorState extends State { // Only wrap with GestureDetector if there are actual gesture callbacks if (hasGestures) { innerChild = GestureDetector( + // Translucent so the whole anchor bounds are tappable, not only the + // opaque descendants. The GestureDetector defaults to + // `HitTestBehavior.deferToChild`, which only receives a pointer when a + // painted child sits under it; an anchor wrapping transparent content + // (a settings row with text on one side, a link, an icon row) then + // ignored taps that landed on its empty regions. Real users tapping the + // blank part of a row, and every centre-of-element tap from a driver + // (dusk / Playwright), silently did nothing. Translucent matches the + // whole-box tap target WInput and WPopover already use. + behavior: HitTestBehavior.translucent, onTap: widget.isDisabled ? null : widget.onTap, onLongPress: widget.isDisabled ? null : widget.onLongPress, onDoubleTap: widget.isDisabled ? null : widget.onDoubleTap, diff --git a/test/widgets/w_anchor_test.dart b/test/widgets/w_anchor_test.dart index 56489de..bc1beac 100644 --- a/test/widgets/w_anchor_test.dart +++ b/test/widgets/w_anchor_test.dart @@ -208,6 +208,39 @@ void main() { expect(tapped, isTrue); }); + testWidgets('onTap fires when tapping a transparent region of the anchor', + (tester) async { + // A settings-row-style anchor: the interactive content (a label) sits + // on the left, leaving the centre/right transparent. With the + // GestureDetector defaulting to deferToChild, a tap on the transparent + // centre missed the gesture entirely, so a real user tapping the blank + // part of a row, and every centre-of-element tap from a driver + // (dusk / Playwright), silently did nothing. The whole anchor bounds + // must be tappable. + bool tapped = false; + await tester.pumpWidget( + wrapWithTheme( + WAnchor( + onTap: () => tapped = true, + child: const SizedBox( + width: 300, + height: 60, + child: Align( + alignment: Alignment.centerLeft, + child: Text('Dark'), + ), + ), + ), + ), + ); + + // Tap the centre of the anchor: a transparent region with no opaque + // child under it (the label is pinned to the far left). + await tester.tapAt(tester.getCenter(find.byType(WAnchor))); + await tester.pump(); + expect(tapped, isTrue); + }); + testWidgets('onLongPress callback fires', (tester) async { bool pressed = false; await tester.pumpWidget(