-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Description
Steps to reproduce
- Create a new Flutter project and replace the contents of lib/main.dart with the code provided in the attached file.
- Run the application on the web: flutter run -d chrome.
- On the "Main Page", click the "Go to Web View" button.
- On the "Web View" page, interact with the embedded website (e.g., click on links, scroll) to confirm it is working correctly.
- Click the back arrow in the AppBar to return to the "Main Page".
- Click the "Show Dialog" button. A modal dialog will appear.
- Dismiss the dialog by clicking on the grayed-out area outside of it (the modal barrier).
- You can click the "Close & Go to Web" button. too
- Click the "Go to Web View" button again.
Expected results
After navigating back to the web view page, the HtmlElementView (containing the iframe) should remain fully interactive and responsive to mouse clicks, just as it was in Step 4.
Actual results
The HtmlElementView is completely unresponsive to all mouse interactions. Clicks do not register on the embedded website. The content is visible, but it cannot be interacted with, as if an invisible layer is blocking all pointer events.
Additional Context and Analysis
Investigation Summary (Attempts that did NOT consistently reproduce the bug)
Before finding a consistent reproduction, several simpler navigation methods were attempted. These setups did not reliably trigger the bug, suggesting the issue is dependent on a specific page lifecycle or state management pattern:
Using TabBarView: A TabBar was used to switch between a tab with the dialog button and a tab with the HtmlElementView. The bug did not consistently appear with this method.
Simple State Swapping: A single StatefulWidget was used to swap between the main page widget and the web view widget using setState. This also failed to reproduce the bug reliably.
The Critical Point of Failure: Navigator 2.0 (RouterDelegate)
The bug only became 100% reproducible after refactoring the navigation to use Flutter's declarative navigation API (Navigator 2.0) with a custom RouterDelegate and RouteInformationParser.
This strongly suggests the root cause is an issue with how the Flutter web engine handles the cleanup of the semantic DOM element for the modal barrier when a page is managed by a RouterDelegate. The barrier's DOM element appears to be left behind invisibly on top of the UI, blocking subsequent pointer events from reaching the HtmlElementView. The combination of SemanticsBinding, a dismissible modal barrier, and the RouterDelegate's page management seems to be the precise recipe for this issue.
Code sample
Code sample
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'dart:ui_web' as ui;
import 'package:web/web.dart' as html;
void main() {
WidgetsFlutterBinding.ensureInitialized();
SemanticsBinding.instance.ensureSemantics();
runApp(MyApp());
}
// Represents the two states of our application's navigation.
class BugReproRoutePath {
final bool isWebView;
BugReproRoutePath.home() : isWebView = false;
BugReproRoutePath.web() : isWebView = true;
}
// Parses route information (URL) into our custom route path class.
class BugReproRouteInformationParser
extends RouteInformationParser<BugReproRoutePath> {
@override
Future<BugReproRoutePath> parseRouteInformation(
RouteInformation routeInformation,
) async {
final uri = Uri.parse(routeInformation.uri.path);
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'web') {
return BugReproRoutePath.web();
} else {
return BugReproRoutePath.home();
}
}
@override
RouteInformation? restoreRouteInformation(BugReproRoutePath configuration) {
if (configuration.isWebView) {
return RouteInformation(uri: Uri.parse('/web'));
} else {
return RouteInformation(uri: Uri.parse('/'));
}
}
}
// Manages the navigator's state and pages based on the current route path.
class BugReproRouterDelegate extends RouterDelegate<BugReproRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BugReproRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
BugReproRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
BugReproRoutePath _currentPath = BugReproRoutePath.home();
@override
BugReproRoutePath get currentConfiguration => _currentPath;
@override
Future<void> setNewRoutePath(BugReproRoutePath configuration) async {
_currentPath = configuration;
}
void showWebView() {
if (!_currentPath.isWebView) {
_currentPath = BugReproRoutePath.web();
notifyListeners();
}
}
void showMain() {
if (_currentPath.isWebView) {
_currentPath = BugReproRoutePath.home();
notifyListeners();
}
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: const ValueKey('MainInstructionsPage'),
child: MainInstructionsPage(
onNavigateToWeb: showWebView,
onShowDialog: _showBugDialog,
),
),
if (_currentPath.isWebView)
MaterialPage(
key: const ValueKey('WebViewPage'),
child: WebViewPage(onClose: showMain),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// This handles the browser back button or AppBar back button on WebViewPage
if (_currentPath.isWebView) {
showMain();
}
return true;
},
);
}
// Helper to show the dialog using the navigator's context.
void _showBugDialog() {
showDialog(
context: navigatorKey.currentContext!,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Dismiss Me'),
content: const Text('Click outside this box to dismiss.'),
actions: <Widget>[
TextButton(
child: const Text('Close & Go to Web'),
onPressed: () {
Navigator.of(context).pop();
showWebView();
},
),
],
);
},
);
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late BugReproRouterDelegate _routerDelegate;
late BugReproRouteInformationParser _routeInformationParser;
@override
void initState() {
super.initState();
_routerDelegate = BugReproRouterDelegate();
_routeInformationParser = BugReproRouteInformationParser();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter IFrame Bug Demo',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
/// The page containing instructions and the dialog trigger.
class MainInstructionsPage extends StatelessWidget {
final VoidCallback onNavigateToWeb;
final VoidCallback onShowDialog;
const MainInstructionsPage({
super.key,
required this.onNavigateToWeb,
required this.onShowDialog,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('IFrame Bug - Main Page'),
backgroundColor: Colors.blue.shade800,
),
body: Container(
alignment: Alignment.center,
color: Colors.grey.shade200,
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Steps to Reproduce:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 12),
const Text('1. Click "Go to Web View" to open the web page.'),
const SizedBox(height: 4),
const Text(
'2. Interact with the website, then press the back button in the app bar.',
),
const SizedBox(height: 4),
const Text('3. Back on this page, click "Show Dialog".'),
const SizedBox(height: 4),
const Text(
'4. CRITICAL: Click the grayed-out barrier to dismiss the dialog.',
),
const SizedBox(height: 4),
const Text(
'5. Click "Go to Web View" again. The website will now be unresponsive.',
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
onPressed: onNavigateToWeb,
child: const Text('Go to Web View'),
),
const SizedBox(width: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
backgroundColor: Colors.red.shade400,
),
onPressed: onShowDialog,
child: const Text('Show Dialog'),
),
],
),
],
),
),
);
}
}
/// A standalone page for the web view.
class WebViewPage extends StatefulWidget {
final VoidCallback onClose;
const WebViewPage({super.key, required this.onClose});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final html.HTMLIFrameElement _iframeElement;
late final String _viewType;
@override
void initState() {
super.initState();
_viewType = 'iframeElement-${DateTime.now().millisecondsSinceEpoch}';
_iframeElement =
html.HTMLIFrameElement()
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..src = 'https://flutter.dev/';
ui.platformViewRegistry.registerViewFactory(
_viewType,
(int viewId) => _iframeElement,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Web View'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: widget.onClose,
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(child: HtmlElementView(viewType: _viewType)),
),
);
}
}Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.35.3, on macOS 15.6.1 24G90 darwin-arm64, locale en-MY) [472ms]
• Flutter version 3.35.3 on channel stable at /Users/kuan/.asdf/installs/flutter/3.35.3-stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision a402d9a437 (6 days ago), 2025-09-03 14:54:31 -0700
• Engine revision ddf47dd3ff
• Dart version 3.9.2
• DevTools version 2.48.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-lldb-debugging
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [6.8s]
• Android SDK at /Users/kuan/Dev/Android/sdk
• Emulator version 35.5.10.0 (build_id 13402964) (CL:N/A)
• Platform android-35, build-tools 34.0.0
• ANDROID_HOME = /Users/Kuan/Dev/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 16.1) [1,895ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16B40
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [7ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2023.2) [6ms]
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
[✓] VS Code (version 1.103.2) [5ms]
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.118.0
[✓] Network resources [1,854ms]
• All expected network resources are available.