-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
flutter/engine
#44014Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)Accessibility, e.g. VoiceOver or TalkBack. (aka a11y)customer: money (g3)p: webviewThe WebView pluginThe WebView pluginpackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyteam-iosOwned by iOS platform teamOwned by iOS platform team
Description
Is there an existing issue for this?
- I have searched the existing issues
- I have read the guide to filing a bug
Steps to reproduce
Copied from internal bug b/292047405 - See bug for more details
Steps to test:
- Build app on simulator
- Open accessibility inspector.
- Inspect the buttons on screen.
- Leave app running and come back to do steps 2 and 3 in a few minutes.
Expected results
The buttons on screen should be highlighted green individually by accessibility inspector.
Actual results
The buttons can no longer be found by accessibility inspector on the status bar is accessible.
Code sample
class WebViewExample extends StatefulWidget {
@override
_WebViewExampleState createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView example'),
// This drop down menu demonstrates that Flutter widgets can be shown over the web view.
actions: <Widget>[
NavigationControls(_controller.future),
SampleMenu(_controller.future),
],
),
// We're using a Builder here so we have a context that is below the Scaffold
// to allow calling Scaffold.of(context) so we can show a snackbar.
body: Builder(
builder: (BuildContext context) {
return WebView(
initialUrl: 'https://flutter.dev',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
webViewController.loadUrl('https://flutter.dev');
},
javascriptChannels: {_toasterJavascriptChannel(context)},
navigationDelegate: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
return NavigationDecision.navigate;
},
onPageFinished: (String url) {
print('Page finished loading: $url');
},
);
},
),
floatingActionButton: favoriteButton(),
);
}
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (JavascriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
},
);
}
Widget favoriteButton() {
return FutureBuilder<WebViewController>(
future: _controller.future,
builder: (
BuildContext context,
AsyncSnapshot<WebViewController> controller,
) {
if (controller.hasData) {
return FloatingActionButton(
onPressed: () async {
final url = await controller.data!.currentUrl();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Favorited $url')),
);
},
child: const Icon(Icons.favorite),
);
}
return Container();
},
);
}
}
enum MenuOptions {
showUserAgent,
listCookies,
clearCookies,
addToCache,
listCache,
clearCache,
navigationDelegate,
}
class SampleMenu extends StatelessWidget {
SampleMenu(this.controller);
final Future<WebViewController> controller;
final CookieManager cookieManager = CookieManager();
@override
Widget build(BuildContext context) {
return FutureBuilder<WebViewController>(
future: controller,
builder:
(BuildContext context, AsyncSnapshot<WebViewController> controller) {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
final data = controller.data!;
switch (value) {
case MenuOptions.showUserAgent:
_onShowUserAgent(data, context);
break;
case MenuOptions.listCookies:
_onListCookies(data, context);
break;
case MenuOptions.clearCookies:
_onClearCookies(context);
break;
case MenuOptions.addToCache:
_onAddToCache(data, context);
break;
case MenuOptions.listCache:
_onListCache(data, context);
break;
case MenuOptions.clearCache:
_onClearCache(data, context);
break;
case MenuOptions.navigationDelegate:
_onNavigationDelegateExample(data, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.showUserAgent,
enabled: controller.hasData,
child: const Text('Show user agent'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCookies,
child: Text('List cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCookies,
child: Text('Clear cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.addToCache,
child: Text('Add to cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCache,
child: Text('List cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCache,
child: Text('Clear cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.navigationDelegate,
child: Text('Navigation Delegate example'),
),
],
);
},
);
}
void _onShowUserAgent(
WebViewController controller,
BuildContext context,
) async {
// Send a message with the user agent string to the Toaster JavaScript channel we registered
// with the WebView.
await controller.evaluateJavascript(
'Toaster.postMessage("User Agent: " + navigator.userAgent);',
);
}
void _onListCookies(
WebViewController controller,
BuildContext context,
) async {
final cookies = await controller.evaluateJavascript('document.cookie');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Cookies:'),
_getCookieList(cookies),
],
),
),
);
}
void _onAddToCache(WebViewController controller, BuildContext context) async {
await controller.evaluateJavascript(
'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";',
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Added a test entry to cache.'),
),
);
}
void _onListCache(WebViewController controller, BuildContext context) async {
await controller.evaluateJavascript(
'caches.keys()'
'.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
'.then((caches) => Toaster.postMessage(caches))',
);
}
void _onClearCache(WebViewController controller, BuildContext context) async {
await controller.clearCache();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cache cleared.'),
),
);
}
void _onClearCookies(BuildContext context) async {
final hadCookies = await cookieManager.clearCookies();
var message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There are no cookies.';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
void _onNavigationDelegateExample(
WebViewController controller,
BuildContext context,
) async {
final contentBase64 =
base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
await controller.loadUrl('data:text/html;base64,$contentBase64');
}
Widget _getCookieList(String cookies) {
if (cookies == null || cookies == '""') {
return Container();
}
final cookieList = cookies.split(';');
final cookieWidgets = cookieList.map((String cookie) => Text(cookie));
return Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: cookieWidgets.toList(),
);
}
}
class NavigationControls extends StatelessWidget {
const NavigationControls(this._webViewControllerFuture)
: assert(_webViewControllerFuture != null);
final Future<WebViewController> _webViewControllerFuture;
@override
Widget build(BuildContext context) {
return FutureBuilder<WebViewController>(
future: _webViewControllerFuture,
builder:
(BuildContext context, AsyncSnapshot<WebViewController> snapshot) {
final webViewReady = snapshot.connectionState == ConnectionState.done;
final controller = snapshot.data;
return Row(
children: <Widget>[
IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: webViewReady
? () async {
if (await controller!.canGoBack()) {
return controller.goBack();
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No back history item')),
);
}
: null,
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: webViewReady
? () async {
if (await controller!.canGoForward()) {
return controller.goForward();
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No forward history item'),
),
);
}
: null,
),
IconButton(
icon: const Icon(Icons.replay),
onPressed: webViewReady ? controller!.reload : null,
),
],
);
},
);
}
}Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[Paste your output here]Metadata
Metadata
Assignees
Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)Accessibility, e.g. VoiceOver or TalkBack. (aka a11y)customer: money (g3)p: webviewThe WebView pluginThe WebView pluginpackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.platform-iosiOS applications specificallyiOS applications specificallyteam-iosOwned by iOS platform teamOwned by iOS platform team