Unverified Commit d5a10340 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Expandable Search (#17629)

parent 711174a9
......@@ -24,6 +24,7 @@ export 'page_selector_demo.dart';
export 'persistent_bottom_sheet_demo.dart';
export 'progress_indicator_demo.dart';
export 'scrollable_tabs_demo.dart';
export 'search_demo.dart';
export 'selection_controls_demo.dart';
export 'slider_demo.dart';
export 'snack_bar_demo.dart';
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class SearchDemo extends StatefulWidget {
static const String routeName = '/material/search';
@override
_SearchDemoState createState() => new _SearchDemoState();
}
class _SearchDemoState extends State<SearchDemo> {
final _SearchDemoSearchDelegate _delegate = new _SearchDemoSearchDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _lastIntegerSelected;
@override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
leading: new IconButton(
tooltip: 'Navigation menu',
icon: new AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
color: Colors.white,
progress: _delegate.transitionAnimation,
),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
title: const Text('Numbers'),
actions: <Widget>[
new IconButton(
tooltip: 'Search',
icon: const Icon(Icons.search),
onPressed: () async {
final int selected = await showSearch<int>(
context: context,
delegate: _delegate,
);
if (selected != null && selected != _lastIntegerSelected) {
setState(() {
_lastIntegerSelected = selected;
});
}
},
),
new IconButton(
tooltip: 'More (not implemented)',
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new MergeSemantics(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
const Text('Press the '),
const Tooltip(
message: 'search',
child: const Icon(
Icons.search,
size: 18.0,
),
),
const Text(' icon in the AppBar'),
],
),
const Text('and search for an integer between 0 and 100,000.'),
],
),
),
const SizedBox(height: 64.0),
new Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE' }.')
],
),
),
floatingActionButton: new FloatingActionButton.extended(
tooltip: 'Back', // Tests depend on this label to exit the demo.
onPressed: () {
Navigator.of(context).pop();
},
label: const Text('Close demo'),
icon: const Icon(Icons.close),
),
drawer: new Drawer(
child: new Column(
children: <Widget>[
const UserAccountsDrawerHeader(
accountName: const Text('Zach Widget'),
accountEmail: const Text('zach.widget@example.com'),
currentAccountPicture: const CircleAvatar(
backgroundImage: const AssetImage(
'shrine/vendors/zach.jpg',
package: 'flutter_gallery_assets',
),
),
margin: EdgeInsets.zero,
),
new MediaQuery.removePadding(
context: context,
// DrawerHeader consumes top MediaQuery padding.
removeTop: true,
child: const ListTile(
leading: const Icon(Icons.payment),
title: const Text('Placeholder'),
),
),
],
),
),
);
}
}
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
final List<int> _data = new List<int>.generate(100001, (int i) => i).reversed.toList();
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
@override
Widget buildLeading(BuildContext context) {
return new IconButton(
tooltip: 'Back',
icon: new AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
},
);
}
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<int> suggestions = query.isEmpty
? _history
: _data.where((int i) => '$i'.startsWith(query));
return new _SuggestionList(
query: query,
suggestions: suggestions.map((int i) => '$i').toList(),
onSelected: (String suggestion) {
query = suggestion;
showResults(context);
},
);
}
@override
Widget buildResults(BuildContext context) {
final int searched = int.tryParse(query);
if (searched == null || !_data.contains(searched)) {
return new Center(
child: new Text(
'"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
textAlign: TextAlign.center,
),
);
}
return new ListView(
children: <Widget>[
new _ResultCard(
title: 'This integer',
integer: searched,
searchDelegate: this,
),
new _ResultCard(
title: 'Next integer',
integer: searched + 1,
searchDelegate: this,
),
new _ResultCard(
title: 'Previous integer',
integer: searched - 1,
searchDelegate: this,
),
],
);
}
@override
List<Widget> buildActions(BuildContext context) {
return <Widget>[
query.isEmpty
? new IconButton(
tooltip: 'Voice Search',
icon: const Icon(Icons.mic),
onPressed: () {
query = 'TODO: implement voice input';
},
)
: new IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
)
];
}
}
class _ResultCard extends StatelessWidget {
const _ResultCard({this.integer, this.title, this.searchDelegate});
final int integer;
final String title;
final SearchDelegate<int> searchDelegate;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return new GestureDetector(
onTap: () {
searchDelegate.close(context, integer);
},
child: new Card(
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new Column(
children: <Widget>[
new Text(title),
new Text(
'$integer',
style: theme.textTheme.headline.copyWith(fontSize: 72.0),
),
],
),
),
),
);
}
}
class _SuggestionList extends StatelessWidget {
const _SuggestionList({this.suggestions, this.query, this.onSelected});
final List<String> suggestions;
final String query;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return new ListView.builder(
itemCount: suggestions.length,
itemBuilder: (BuildContext context, int i) {
final String suggestion = suggestions[i];
return new ListTile(
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
title: new RichText(
text: new TextSpan(
text: suggestion.substring(0, query.length),
style: theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
children: <TextSpan>[
new TextSpan(
text: suggestion.substring(query.length),
style: theme.textTheme.subhead,
),
],
),
),
onTap: () {
onSelected(suggestion);
},
);
},
);
}
}
......@@ -313,6 +313,14 @@ List<GalleryDemo> _buildGalleryDemos() {
routeName: OverscrollDemo.routeName,
buildRoute: (BuildContext context) => const OverscrollDemo(),
),
new GalleryDemo(
title: 'Search',
subtitle: 'Expandable search',
icon: Icons.search,
category: _kMaterialComponents,
routeName: SearchDemo.routeName,
buildRoute: (BuildContext context) => new SearchDemo(),
),
new GalleryDemo(
title: 'Selection controls',
subtitle: 'Checkboxes, radio buttons, and switches',
......
......@@ -78,6 +78,7 @@ export 'src/material/raised_button.dart';
export 'src/material/refresh_indicator.dart';
export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart';
export 'src/material/search.dart';
export 'src/material/shadows.dart';
export 'src/material/slider.dart';
export 'src/material/slider_theme.dart';
......
......@@ -160,6 +160,10 @@ abstract class MaterialLocalizations {
/// alert dialog widget is opened.
String get alertDialogLabel;
/// Label indicating that a text field is a search field. This will be used
/// as a hint text in the text field.
String get searchFieldLabel;
/// The format used to lay out the time picker.
///
/// The documentation for [TimeOfDayFormat] enum values provides details on
......@@ -537,6 +541,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
String get alertDialogLabel => 'Alert';
@override
String get searchFieldLabel => 'Search';
@override
String aboutListTileTitle(String applicationName) => 'About $applicationName';
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'app_bar.dart';
import 'colors.dart';
import 'input_border.dart';
import 'input_decorator.dart';
import 'material_localizations.dart';
import 'scaffold.dart';
import 'text_field.dart';
import 'theme.dart';
/// Shows a full screen search page and returns the search result selected by
/// the user when the page is closed.
///
/// The search page consists of an app bar with a search field and a body which
/// can either show suggested search queries or the search results.
///
/// The appearance of the search page is determined by the provided
/// `delegate`. The initial query string is given by `query`, which defaults
/// to the empty string. When `query` is set to null, `delegate.query` will
/// be used as the initial query.
///
/// The method returns the selected search result, which can be set in the
/// [SearchDelegate.close] call.
///
/// A given [SearchDelegate] can only be associated with one active [showSearch]
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
/// for another [showSearch] call.
///
/// The transition to the search page triggered by this method looks best if the
/// screen triggering the transition contains an [AppBar] at the top and the
/// transition is called from an [IconButton] that's part of [AppBar.actions].
/// The animation provided by [SearchDelegate.transitionAnimation] can be used
/// to trigger additional animations in the underlying page while the search
/// page fades in or out. This is commonly used to animate an [AnimatedIcon] in
/// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow
/// used to exit the search page.
///
/// See also:
///
/// * [SearchDelegate] to define the content of the search page.
Future<T> showSearch<T>({
@required BuildContext context,
@required SearchDelegate<T> delegate,
String query: '',
}) {
assert(delegate != null);
assert(context != null);
assert(delegate._result == null || delegate._result.isCompleted);
delegate._result = new Completer<T>();
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
Navigator.of(context).push(new _SearchPageRoute<T>(
delegate: delegate,
));
return delegate._result.future;
}
/// Delegate for [showSearch] to define the content of the search page.
///
/// The search page always shows an [AppBar] at the top where users can
/// enter their search queries. The buttons shown before and after the search
/// query text field can be customized via [SearchDelegate.leading] and
/// [SearchDelegate.actions].
///
/// The body below the [AppBar] can either show suggested queries (returned by
/// [SearchDelegate.buildSuggestions]) or - once the user submits a search - the
/// results of the search as returned by [SearchDelegate.buildResults].
///
/// [SearchDelegate.query] always contains the current query entered by the user
/// and should be used to build the suggestions and results.
///
/// The results can be brought on screen by calling [SearchDelegate.showResults]
/// and you can go back to showing the suggestions by calling
/// [SearchDelegate.showSuggestions].
///
/// Once the user has selected a search result, [SearchDelegate.close] should be
/// called to remove the search page from the top of the navigation stack and
/// to notify the caller of [showSearch] about the selected search result.
///
/// A given [SearchDelegate] can only be associated with one active [showSearch]
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
/// for another [showSearch] call.
abstract class SearchDelegate<T> {
/// Suggestions shown in the body of the search page while the user types a
/// query into the search field.
///
/// The delegate method is called whenever the content of [query] changes.
/// The suggestions should be based on the current [query] string. If the query
/// string is empty, it is good practice to show suggested queries based on
/// past queries or the current context.
///
/// Usually, this method will return a [ListView] with one [ListTile] per
/// suggestion. When [ListTile.onTap] is called, [query] should be updated
/// with the corresponding suggestion and the results page should be shown
/// by calling [showResults].
Widget buildSuggestions(BuildContext context);
/// The results shown after the user submits a search from the search page.
///
/// The current value of [query] can be used to determine what the user
/// searched for.
///
/// Typically, this method returns a [ListView] with the search results.
/// When the user taps on a particular search result, [close] should be called
/// with the selected result as argument. This will close the search page and
/// communicate the result back to the initial caller of [showSearch].
Widget buildResults(BuildContext context);
/// A widget to display before the current query in the [AppBar].
///
/// Typically an [IconButton] configured with a [BackButtonIcon] that exits
/// the search with [close]. One can also use an [AnimatedIcon] driven by
/// [transitionAnimation], which animates from e.g. a hamburger menu to the
/// back button as the search overlay fades in.
///
/// Returns null if no widget should be shown.
///
/// See also:
///
/// * [AppBar.leading], the intended use for the return value of this method.
Widget buildLeading(BuildContext context);
/// Widgets to display after the search query in the [AppBar].
///
/// If the [query] is not empty, this should typically contain a button to
/// clear the query and show the suggestions again (via [showSuggestions]) if
/// the results are currently shown.
///
/// Returns null if no widget should be shown
///
/// See also:
///
/// * [AppBar.actions], the intended use for the return value of this method.
List<Widget> buildActions(BuildContext context);
/// The theme used to style the [AppBar].
///
/// By default, a white theme is used.
///
/// See also:
///
/// * [AppBar.backgroundColor], which is set to [ThemeData.primaryColor].
/// * [Appbar.iconTheme], which is set to [ThemeData.primaryIconTheme].
/// * [AppBar.textTheme], which is set to [ThemeData.primaryTextTheme].
/// * [AppBar.brightness], which is set to [ThemeData.primaryColorBrightness].
ThemeData appBarTheme(BuildContext context) {
assert(context != null);
final ThemeData theme = Theme.of(context);
assert(theme != null);
return theme.copyWith(
primaryColor: Colors.white,
primaryIconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
primaryColorBrightness: Brightness.light,
primaryTextTheme: theme.textTheme,
);
}
/// The current query string shown in the [Appbar].
///
/// The user manipulates this string via the keyboard.
///
/// If the user taps on a suggestion provided by [buildSuggestions] this
/// string should be updated to that suggestion via the setter.
String get query => _queryTextController.text;
set query(String value) {
assert(query != null);
_queryTextController.text = value;
}
/// Transition from the suggestions returned by [buildSuggestions] to the
/// [query] results returned by [buildResults].
///
/// If the user taps on a suggestion provided by [buildSuggestions] the
/// screen should typically transition to the page showing the search
/// results for the suggested query. This transition can be triggered
/// by calling this method.
///
/// See also:
///
/// * [showSuggestions] to show the search suggestions again.
void showResults(BuildContext context) {
_focusNode.unfocus();
_currentBody = _SearchBody.results;
}
/// Transition from showing the results returned by [buildResults] to showing
/// the suggestions returned by [buildSuggestions].
///
/// Calling this method will also put the input focus back into the search
/// field of the ApBar.
///
/// If the results are currently shown this method can be used to go back
/// to showing the search suggestions.
///
/// See also:
///
/// * [showResults] to show the search results.
void showSuggestions(BuildContext context) {
FocusScope.of(context).requestFocus(_focusNode);
_currentBody = _SearchBody.suggestions;
}
/// Closes the search page and returns to the underlying route.
///
/// The value provided for `result` is used as the return value of the call
/// to [showSearch] that launched the search initially.
void close(BuildContext context, T result) {
_currentBody = null;
_focusNode.unfocus();
_result.complete(result);
Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route)
..pop(result);
}
/// [Animation] triggered when the search pages fades in or out.
///
/// This animation is commonly used to animate [AnimatedIcon]s of
/// [IconButton]s returned by [buildLeading] or [buildActions]. It can also be
/// used to animate [IconButton]s contained within the route below the search
/// page.
Animation<double> get transitionAnimation => _proxyAnimation;
final FocusNode _focusNode = new FocusNode();
final TextEditingController _queryTextController = new TextEditingController();
final ProxyAnimation _proxyAnimation = new ProxyAnimation(kAlwaysDismissedAnimation);
final ValueNotifier<_SearchBody> _currentBodyNotifier = new ValueNotifier<_SearchBody>(null);
_SearchBody get _currentBody => _currentBodyNotifier.value;
set _currentBody(_SearchBody value) {
_currentBodyNotifier.value = value;
}
Completer<T> _result;
_SearchPageRoute<T> _route;
}
/// Describes the body that is currently shown under the [AppBar] in the
/// search page.
enum _SearchBody {
/// Suggested queries are shown in the body.
///
/// The suggested queries are generated by [SearchDelegate.buildSuggestions].
suggestions,
/// Search results are currently shown in the body.
///
/// The search results are generated by [SearchDelegate.buildResults].
results,
}
class _SearchPageRoute<T> extends PageRoute<void> {
_SearchPageRoute({
@required this.delegate,
}) : assert(delegate != null) {
assert(
delegate._route == null,
'The ${delegate.runtimeType} instance is currently used by another active '
'search. Please close that search by calling close() on the SearchDelegate '
'before openening another search with the same delegate instance.',
);
delegate._route = this;
}
final SearchDelegate<T> delegate;
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get maintainState => false;
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return new FadeTransition(
opacity: animation,
child: child,
);
}
@override
Animation<double> createAnimation() {
final Animation<double> animation = super.createAnimation();
delegate._proxyAnimation.parent = animation;
return animation;
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return new _SearchPage<T>(
delegate: delegate,
animation: animation,
);
}
@override
void didComplete(void result) {
super.didComplete(result);
assert(delegate._route == this);
delegate._route = null;
}
}
class _SearchPage<T> extends StatefulWidget {
const _SearchPage({
this.delegate,
this.animation,
});
final SearchDelegate<T> delegate;
final Animation<double> animation;
@override
State<StatefulWidget> createState() => new _SearchPageState<T>();
}
class _SearchPageState<T> extends State<_SearchPage<T>> {
@override
void initState() {
super.initState();
queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate._focusNode.addListener(_onFocusChanged);
}
@override
void dispose() {
super.dispose();
queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode.removeListener(_onFocusChanged);
}
void _onAnimationStatusChanged(AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) {
FocusScope.of(context).requestFocus(widget.delegate._focusNode);
}
}
void _onFocusChanged() {
if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
}
}
void _onQueryChanged() {
setState(() {
// rebuild ourselves because query changed.
});
}
void _onSearchBodyChanged() {
setState(() {
// rebuild ourselves because search body changed.
});
}
@override
Widget build(BuildContext context) {
final ThemeData theme = widget.delegate.appBarTheme(context);
Widget body;
switch(widget.delegate._currentBody) {
case _SearchBody.suggestions:
body = new KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
break;
case _SearchBody.results:
body = new KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.results),
child: widget.delegate.buildResults(context),
);
break;
}
return new Scaffold(
appBar: new AppBar(
backgroundColor: theme.primaryColor,
iconTheme: theme.primaryIconTheme,
textTheme: theme.primaryTextTheme,
brightness: theme.primaryColorBrightness,
leading: widget.delegate.buildLeading(context),
// TODO(goderbauer): Show the search key (instead of enter) on keyboard, https://github.com/flutter/flutter/issues/17525
title: new TextField(
controller: queryTextController,
focusNode: widget.delegate._focusNode,
style: theme.textTheme.title,
onSubmitted: (String _) {
widget.delegate.showResults(context);
},
decoration: new InputDecoration(
border: InputBorder.none,
hintText: MaterialLocalizations.of(context).searchFieldLabel,
),
),
actions: widget.delegate.buildActions(context),
),
body: new AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
);
}
TextEditingController get queryTextController => widget.delegate._queryTextController;
}
......@@ -34,6 +34,7 @@ void main() {
expect(localizations.popupMenuLabel, isNotNull);
expect(localizations.dialogLabel, isNotNull);
expect(localizations.alertDialogLabel, isNotNull);
expect(localizations.searchFieldLabel, isNotNull);
expect(localizations.aboutListTileTitle('FOO'), isNotNull);
expect(localizations.aboutListTileTitle('FOO'), contains('FOO'));
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open and close search', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
final List<String> selectedResults = <String>[];
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
results: selectedResults,
));
// We are on the homepage
expect(find.text('HomeBody'), findsOneWidget);
expect(find.text('HomeTitle'), findsOneWidget);
expect(find.text('Suggestions'), findsNothing);
// Open search
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsNothing);
expect(find.text('HomeTitle'), findsNothing);
expect(find.text('Suggestions'), findsOneWidget);
expect(selectedResults, hasLength(0));
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isTrue);
// Close search
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsOneWidget);
expect(find.text('HomeTitle'), findsOneWidget);
expect(find.text('Suggestions'), findsNothing);
expect(selectedResults, <String>['Result']);
});
testWidgets('Requests suggestions', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(delegate.query, '');
expect(delegate.querysForSuggestions.last, '');
expect(delegate.querysForResults, hasLength(0));
// Type W o w into search field
delegate.querysForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'W');
await tester.pumpAndSettle();
expect(delegate.query, 'W');
await tester.enterText(find.byType(TextField), 'Wo');
await tester.pumpAndSettle();
expect(delegate.query, 'Wo');
await tester.enterText(find.byType(TextField), 'Wow');
await tester.pumpAndSettle();
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['W', 'Wo', 'Wow']);
expect(delegate.querysForResults, hasLength(0));
});
testWidgets('Shows Results and closes search', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
final List<String> selectedResults = <String>[];
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
results: selectedResults,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'Wow');
await tester.pumpAndSettle();
await tester.tap(find.text('Suggestions'));
await tester.pumpAndSettle();
// We are on the results page for Wow
expect(find.text('HomeBody'), findsNothing);
expect(find.text('HomeTitle'), findsNothing);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Results'), findsOneWidget);
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
expect(delegate.querysForResults, <String>['Wow']);
// Close search
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsOneWidget);
expect(find.text('HomeTitle'), findsOneWidget);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Results'), findsNothing);
expect(selectedResults, <String>['Result']);
});
testWidgets('Can switch between results and suggestions',
(WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
// Showing suggestions
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Results'), findsNothing);
// Typing query Wow
delegate.querysForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'Wow');
await tester.pumpAndSettle();
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, hasLength(0));
await tester.tap(find.text('Suggestions'));
await tester.pumpAndSettle();
// Showing Results
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, <String>['Wow']);
TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
// Taping search field to go back to suggestions
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isTrue);
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Results'), findsNothing);
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow']);
expect(delegate.querysForResults, <String>['Wow']);
await tester.enterText(find.byType(TextField), 'Foo');
await tester.pumpAndSettle();
expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow']);
// Go to results again
await tester.tap(find.text('Suggestions'));
await tester.pumpAndSettle();
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow', 'Foo']);
textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
});
testWidgets('Fresh search allways starts with empty query',
(WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(delegate.query, '');
delegate.query = 'Foo';
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(delegate.query, '');
});
testWidgets('Initial queries are honored', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
expect(delegate.query, '');
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
passInInitialQuery: true,
initialQuery: 'Foo',
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(delegate.query, 'Foo');
});
testWidgets('Initial query null re-used previous query', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
delegate.query = 'Foo';
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
passInInitialQuery: true,
initialQuery: null,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(delegate.query, 'Foo');
});
testWidgets('Changing query shows up in search field', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
passInInitialQuery: true,
initialQuery: null,
));
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
delegate.query = 'Foo';
expect(find.text('Foo'), findsOneWidget);
expect(find.text('Bar'), findsNothing);
delegate.query = 'Bar';
expect(find.text('Foo'), findsNothing);
expect(find.text('Bar'), findsOneWidget);
});
testWidgets('transitionAnimation runs while search fades in/out', (WidgetTester tester) async {
final _TestSearchDelegate delegate = new _TestSearchDelegate();
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
passInInitialQuery: true,
initialQuery: null,
));
// runs while search fades in
expect(delegate.transitionAnimation.status, AnimationStatus.dismissed);
await tester.tap(find.byTooltip('Search'));
expect(delegate.transitionAnimation.status, AnimationStatus.forward);
await tester.pumpAndSettle();
expect(delegate.transitionAnimation.status, AnimationStatus.completed);
// does not run while switching to results
await tester.tap(find.text('Suggestions'));
expect(delegate.transitionAnimation.status, AnimationStatus.completed);
await tester.pumpAndSettle();
expect(delegate.transitionAnimation.status, AnimationStatus.completed);
// runs while search fades out
await tester.tap(find.byTooltip('Back'));
expect(delegate.transitionAnimation.status, AnimationStatus.reverse);
await tester.pumpAndSettle();
expect(delegate.transitionAnimation.status, AnimationStatus.dismissed);
});
testWidgets('Closing nested search returns to search', (WidgetTester tester) async {
final List<String> nestedSearchResults = <String>[];
final _TestSearchDelegate nestedSearchDelegate = new _TestSearchDelegate(
suggestions: 'Nested Suggestions',
result: 'Nested Result',
);
final List<String> selectedResults = <String>[];
final _TestSearchDelegate delegate = new _TestSearchDelegate(
actions: <Widget>[
new Builder(
builder: (BuildContext context) {
return new IconButton(
tooltip: 'Nested Search',
icon: const Icon(Icons.search),
onPressed: () async {
final String result = await showSearch(
context: context,
delegate: nestedSearchDelegate,
);
nestedSearchResults.add(result);
},
);
},
)
],
);
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
results: selectedResults,
));
expect(find.text('HomeBody'), findsOneWidget);
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsNothing);
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Nested Suggestions'), findsNothing);
await tester.tap(find.byTooltip('Nested Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsNothing);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Nested Suggestions'), findsOneWidget);
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(nestedSearchResults, <String>['Nested Result']);
expect(find.text('HomeBody'), findsNothing);
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Nested Suggestions'), findsNothing);
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsOneWidget);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Nested Suggestions'), findsNothing);
expect(selectedResults, <String>['Result']);
});
testWidgets('Closing search with nested search shown goes back to underlying route', (WidgetTester tester) async {
_TestSearchDelegate delegate;
final List<String> nestedSearchResults = <String>[];
final _TestSearchDelegate nestedSearchDelegate = new _TestSearchDelegate(
suggestions: 'Nested Suggestions',
result: 'Nested Result',
actions: <Widget>[
new Builder(
builder: (BuildContext context) {
return new IconButton(
tooltip: 'Close Search',
icon: const Icon(Icons.close),
onPressed: () async {
delegate.close(context, 'Result Foo');
},
);
},
)
],
);
final List<String> selectedResults = <String>[];
delegate = new _TestSearchDelegate(
actions: <Widget>[
new Builder(
builder: (BuildContext context) {
return new IconButton(
tooltip: 'Nested Search',
icon: const Icon(Icons.search),
onPressed: () async {
final String result = await showSearch(
context: context,
delegate: nestedSearchDelegate,
);
nestedSearchResults.add(result);
},
);
},
)
],
);
await tester.pumpWidget(new TestHomePage(
delegate: delegate,
results: selectedResults,
));
expect(find.text('HomeBody'), findsOneWidget);
await tester.tap(find.byTooltip('Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsNothing);
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Nested Suggestions'), findsNothing);
await tester.tap(find.byTooltip('Nested Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsNothing);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Nested Suggestions'), findsOneWidget);
await tester.tap(find.byTooltip('Close Search'));
await tester.pumpAndSettle();
expect(find.text('HomeBody'), findsOneWidget);
expect(find.text('Suggestions'), findsNothing);
expect(find.text('Nested Suggestions'), findsNothing);
expect(nestedSearchResults, hasLength(0));
expect(selectedResults, <String>['Result Foo']);
});
}
class TestHomePage extends StatelessWidget {
const TestHomePage({
this.results,
this.delegate,
this.passInInitialQuery: false,
this.initialQuery,
});
final List<String> results;
final SearchDelegate<String> delegate;
final bool passInInitialQuery;
final String initialQuery;
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: Builder(builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('HomeTitle'),
actions: <Widget>[
new IconButton(
tooltip: 'Search',
icon: const Icon(Icons.search),
onPressed: () async {
String selectedResult;
if (passInInitialQuery) {
selectedResult = await showSearch<String>(
context: context,
delegate: delegate,
query: initialQuery,
);
} else {
selectedResult = await showSearch<String>(
context: context,
delegate: delegate,
);
}
results?.add(selectedResult);
},
),
],
),
body: const Text('HomeBody'),
);
}),
);
}
}
class _TestSearchDelegate extends SearchDelegate<String> {
_TestSearchDelegate({
this.suggestions: 'Suggestions',
this.result: 'Result',
this.actions: const <Widget>[],
});
final String suggestions;
final String result;
final List<Widget> actions;
@override
Widget buildLeading(BuildContext context) {
return new IconButton(
tooltip: 'Back',
icon: const Icon(Icons.arrow_back),
onPressed: () {
close(context, result);
},
);
}
final List<String> querysForSuggestions = <String>[];
final List<String> querysForResults = <String>[];
@override
Widget buildSuggestions(BuildContext context) {
querysForSuggestions.add(query);
return new MaterialButton(
onPressed: () {
showResults(context);
},
child: new Text(suggestions),
);
}
@override
Widget buildResults(BuildContext context) {
querysForResults.add(query);
return const Text('Results');
}
@override
List<Widget> buildActions(BuildContext context) {
return actions;
}
}
......@@ -64,6 +64,7 @@ class TranslationBundle {
String get popupMenuLabel => parent?.popupMenuLabel;
String get dialogLabel => parent?.dialogLabel;
String get alertDialogLabel => parent?.alertDialogLabel;
String get searchFieldLabel => parent?.searchFieldLabel;
}
// ignore: camel_case_types
......@@ -113,6 +114,7 @@ class _Bundle_ar extends TranslationBundle {
@override String get popupMenuLabel => r'قائمة منبثقة';
@override String get dialogLabel => r'مربع حوار';
@override String get alertDialogLabel => r'مربع حوار التنبيه';
@override String get searchFieldLabel => r'بحث';
}
// ignore: camel_case_types
......@@ -159,6 +161,7 @@ class _Bundle_de extends TranslationBundle {
@override String get popupMenuLabel => r'Pop-up-Menü';
@override String get dialogLabel => r'Dialogfeld';
@override String get alertDialogLabel => r'Aufmerksam';
@override String get searchFieldLabel => r'Suchen';
}
// ignore: camel_case_types
......@@ -205,6 +208,7 @@ class _Bundle_en extends TranslationBundle {
@override String get popupMenuLabel => r'Popup menu';
@override String get dialogLabel => r'Dialog';
@override String get alertDialogLabel => r'Alert';
@override String get searchFieldLabel => r'Search';
}
// ignore: camel_case_types
......@@ -251,6 +255,7 @@ class _Bundle_es extends TranslationBundle {
@override String get popupMenuLabel => r'Menú emergente';
@override String get dialogLabel => r'Cuadro de diálogo';
@override String get alertDialogLabel => r'Alerta';
@override String get searchFieldLabel => r'Buscar';
}
// ignore: camel_case_types
......@@ -296,6 +301,7 @@ class _Bundle_fa extends TranslationBundle {
@override String get popupMenuLabel => r'منوی بازشو';
@override String get dialogLabel => r'کادر گفتگو';
@override String get alertDialogLabel => r'هشدار';
@override String get searchFieldLabel => r'جستجو کردن';
}
// ignore: camel_case_types
......@@ -342,6 +348,7 @@ class _Bundle_fr extends TranslationBundle {
@override String get popupMenuLabel => r'Menu contextuel';
@override String get dialogLabel => r'Boîte de dialogue';
@override String get alertDialogLabel => r'Alerte';
@override String get searchFieldLabel => r'Chercher';
}
// ignore: camel_case_types
......@@ -387,6 +394,7 @@ class _Bundle_gsw extends TranslationBundle {
@override String get popupMenuLabel => r'Pop-up-Menü';
@override String get dialogLabel => r'Dialogfeld';
@override String get alertDialogLabel => r'Aufmerksam';
@override String get searchFieldLabel => r'Suchen';
}
// ignore: camel_case_types
......@@ -434,6 +442,7 @@ class _Bundle_he extends TranslationBundle {
@override String get popupMenuLabel => r'תפריט קופץ';
@override String get dialogLabel => r'תיבת דו-שיח';
@override String get alertDialogLabel => r'עֵרָנִי';
@override String get searchFieldLabel => r'לחפש';
}
// ignore: camel_case_types
......@@ -479,6 +488,7 @@ class _Bundle_id extends TranslationBundle {
@override String get popupMenuLabel => r'Menu pop-up';
@override String get dialogLabel => r'Dialog';
@override String get alertDialogLabel => r'Waspada';
@override String get searchFieldLabel => r'Pencarian';
}
// ignore: camel_case_types
......@@ -524,6 +534,7 @@ class _Bundle_it extends TranslationBundle {
@override String get popupMenuLabel => r'Menu popup';
@override String get dialogLabel => r'Finestra di dialogo';
@override String get alertDialogLabel => r'Mettere in guardia';
@override String get searchFieldLabel => r'Ricerca';
}
// ignore: camel_case_types
......@@ -569,6 +580,7 @@ class _Bundle_ja extends TranslationBundle {
@override String get popupMenuLabel => r'ポップアップ メニュー';
@override String get dialogLabel => r'ダイアログ';
@override String get alertDialogLabel => r'アラート';
@override String get searchFieldLabel => r'サーチ';
}
// ignore: camel_case_types
......@@ -614,6 +626,7 @@ class _Bundle_ko extends TranslationBundle {
@override String get popupMenuLabel => r'팝업 메뉴';
@override String get dialogLabel => r'대화상자';
@override String get alertDialogLabel => r'경보';
@override String get searchFieldLabel => r'수색';
}
// ignore: camel_case_types
......@@ -660,6 +673,7 @@ class _Bundle_ms extends TranslationBundle {
@override String get popupMenuLabel => r'Menu pop timbul';
@override String get dialogLabel => r'Dialog';
@override String get alertDialogLabel => r'Amaran';
@override String get searchFieldLabel => r'Carian';
}
// ignore: camel_case_types
......@@ -705,6 +719,7 @@ class _Bundle_nb extends TranslationBundle {
@override String get popupMenuLabel => r'Forgrunnsmeny';
@override String get dialogLabel => r'Dialogboks';
@override String get alertDialogLabel => r'Varsling';
@override String get searchFieldLabel => r'Søke';
}
// ignore: camel_case_types
......@@ -750,6 +765,7 @@ class _Bundle_nl extends TranslationBundle {
@override String get popupMenuLabel => r'Pop-upmenu';
@override String get dialogLabel => r'Dialoogvenster';
@override String get alertDialogLabel => r'Alarm';
@override String get searchFieldLabel => r'Zoeken';
}
// ignore: camel_case_types
......@@ -797,6 +813,7 @@ class _Bundle_pl extends TranslationBundle {
@override String get popupMenuLabel => r'Wyskakujące menu';
@override String get dialogLabel => r'Okno dialogowe';
@override String get alertDialogLabel => r'Alarm';
@override String get searchFieldLabel => r'Szukaj';
}
// ignore: camel_case_types
......@@ -839,6 +856,7 @@ class _Bundle_ps extends TranslationBundle {
@override String get popupMenuLabel => r'د پاپ اپ مینو';
@override String get dialogLabel => r'خبرې اترې';
@override String get alertDialogLabel => r'خبرتیا';
@override String get searchFieldLabel => r'لټون';
}
// ignore: camel_case_types
......@@ -884,6 +902,7 @@ class _Bundle_pt extends TranslationBundle {
@override String get popupMenuLabel => r'Menu pop-up';
@override String get dialogLabel => r'Caixa de diálogo';
@override String get alertDialogLabel => r'Alerta';
@override String get searchFieldLabel => r'Pesquisa';
}
// ignore: camel_case_types
......@@ -931,6 +950,7 @@ class _Bundle_ro extends TranslationBundle {
@override String get popupMenuLabel => r'Meniu pop-up';
@override String get dialogLabel => r'Casetă de dialog';
@override String get alertDialogLabel => r'Alerta';
@override String get searchFieldLabel => r'Căutare';
}
// ignore: camel_case_types
......@@ -979,6 +999,7 @@ class _Bundle_ru extends TranslationBundle {
@override String get popupMenuLabel => r'Всплывающее меню';
@override String get dialogLabel => r'Диалоговое окно';
@override String get alertDialogLabel => r'бдительный';
@override String get searchFieldLabel => r'Поиск';
}
// ignore: camel_case_types
......@@ -1024,6 +1045,7 @@ class _Bundle_th extends TranslationBundle {
@override String get popupMenuLabel => r'เมนูป๊อปอัป';
@override String get dialogLabel => r'กล่องโต้ตอบ';
@override String get alertDialogLabel => r'เตือนภัย';
@override String get searchFieldLabel => r'ค้นหา';
}
// ignore: camel_case_types
......@@ -1069,6 +1091,7 @@ class _Bundle_tr extends TranslationBundle {
@override String get popupMenuLabel => r'Popup menü';
@override String get dialogLabel => r'İletişim kutusu';
@override String get alertDialogLabel => r'Alarm';
@override String get searchFieldLabel => r'Arama';
}
// ignore: camel_case_types
......@@ -1114,6 +1137,7 @@ class _Bundle_ur extends TranslationBundle {
@override String get popupMenuLabel => r'پاپ اپ مینو';
@override String get dialogLabel => r'ڈائیلاگ';
@override String get alertDialogLabel => r'انتباہ';
@override String get searchFieldLabel => r'تلاش کریں';
}
// ignore: camel_case_types
......@@ -1159,6 +1183,7 @@ class _Bundle_vi extends TranslationBundle {
@override String get popupMenuLabel => r'Menu bật lên';
@override String get dialogLabel => r'Hộp thoại';
@override String get alertDialogLabel => r'Hộp thoại';
@override String get searchFieldLabel => r'Tìm kiếm';
}
// ignore: camel_case_types
......@@ -1204,6 +1229,7 @@ class _Bundle_zh extends TranslationBundle {
@override String get popupMenuLabel => r'弹出菜单';
@override String get dialogLabel => r'对话框';
@override String get alertDialogLabel => r'警报';
@override String get searchFieldLabel => r'搜索';
}
// ignore: camel_case_types
......
......@@ -42,5 +42,6 @@
"drawerLabel": "قائمة تنقل",
"popupMenuLabel": "قائمة منبثقة",
"dialogLabel": "مربع حوار",
"alertDialogLabel": "مربع حوار التنبيه"
"alertDialogLabel": "مربع حوار التنبيه",
"searchFieldLabel": "بحث"
}
......@@ -39,5 +39,6 @@
"drawerLabel": "Navigationsmenü",
"popupMenuLabel": "Pop-up-Menü",
"dialogLabel": "Dialogfeld",
"alertDialogLabel": "Aufmerksam"
"alertDialogLabel": "Aufmerksam",
"searchFieldLabel": "Suchen"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigationsmenü",
"popupMenuLabel": "Pop-up-Menü",
"dialogLabel": "Dialogfeld",
"alertDialogLabel": "Aufmerksam"
"alertDialogLabel": "Aufmerksam",
"searchFieldLabel": "Suchen"
}
......@@ -199,5 +199,10 @@
"alertDialogLabel": "Alert",
"@dialogLabel": {
"description": "The audio announcement made when an AlertDialog is opened."
},
"searchFieldLabel": "Search",
"@dialogLabel": {
"description": "Label indicating that a text field is a search field. This will be used as a hint text in the text field."
}
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigation menu",
"popupMenuLabel": "Pop-up menu",
"dialogLabel": "Dialogue",
"alertDialogLabel": "Alert"
"alertDialogLabel": "Alert",
"searchFieldLabel": "Search"
}
......@@ -39,5 +39,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Cuadro de diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menú de navegación",
"popupMenuLabel": "Menú emergente",
"dialogLabel": "Diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Buscar"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "منوی پیمایش",
"popupMenuLabel": "منوی بازشو",
"dialogLabel": "کادر گفتگو",
"alertDialogLabel": "هشدار"
"alertDialogLabel": "هشدار",
"searchFieldLabel": "جستجو کردن"
}
......@@ -39,5 +39,6 @@
"drawerLabel": "Menu de navigation",
"popupMenuLabel": "Menu contextuel",
"dialogLabel": "Boîte de dialogue",
"alertDialogLabel": "Alerte"
"alertDialogLabel": "Alerte",
"searchFieldLabel": "Chercher"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigationsmenü",
"popupMenuLabel": "Pop-up-Menü",
"dialogLabel": "Dialogfeld",
"alertDialogLabel": "Aufmerksam"
"alertDialogLabel": "Aufmerksam",
"searchFieldLabel": "Suchen"
}
......@@ -40,5 +40,6 @@
"drawerLabel": "תפריט ניווט",
"popupMenuLabel": "תפריט קופץ",
"dialogLabel": "תיבת דו-שיח",
"alertDialogLabel": "עֵרָנִי"
"alertDialogLabel": "עֵרָנִי",
"searchFieldLabel": "לחפש"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menu navigasi",
"popupMenuLabel": "Menu pop-up",
"dialogLabel": "Dialog",
"alertDialogLabel": "Waspada"
"alertDialogLabel": "Waspada",
"searchFieldLabel": "Pencarian"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menu di navigazione",
"popupMenuLabel": "Menu popup",
"dialogLabel": "Finestra di dialogo",
"alertDialogLabel": "Mettere in guardia"
"alertDialogLabel": "Mettere in guardia",
"searchFieldLabel": "Ricerca"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "ナビゲーション メニュー",
"popupMenuLabel": "ポップアップ メニュー",
"dialogLabel": "ダイアログ",
"alertDialogLabel": "アラート"
"alertDialogLabel": "アラート",
"searchFieldLabel": "サーチ"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "탐색 메뉴",
"popupMenuLabel": "팝업 메뉴",
"dialogLabel": "대화상자",
"alertDialogLabel": "경보"
"alertDialogLabel": "경보",
"searchFieldLabel": "수색"
}
......@@ -39,5 +39,6 @@
"drawerLabel": "Menu navigasi",
"popupMenuLabel": "Menu pop timbul",
"dialogLabel": "Dialog",
"alertDialogLabel": "Amaran"
"alertDialogLabel": "Amaran",
"searchFieldLabel": "Carian"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigasjonsmeny",
"popupMenuLabel": "Forgrunnsmeny",
"dialogLabel": "Dialogboks",
"alertDialogLabel": "Varsling"
"alertDialogLabel": "Varsling",
"searchFieldLabel": "Søke"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Navigatiemenu",
"popupMenuLabel": "Pop-upmenu",
"dialogLabel": "Dialoogvenster",
"alertDialogLabel": "Alarm"
"alertDialogLabel": "Alarm",
"searchFieldLabel": "Zoeken"
}
......@@ -40,5 +40,6 @@
"drawerLabel": "Menu nawigacyjne",
"popupMenuLabel": "Wyskakujące menu",
"dialogLabel": "Okno dialogowe",
"alertDialogLabel": "Alarm"
"alertDialogLabel": "Alarm",
"searchFieldLabel": "Szukaj"
}
......@@ -37,5 +37,6 @@
"drawerLabel": "د نیویگیشن مینو",
"popupMenuLabel": "د پاپ اپ مینو",
"dialogLabel": "خبرې اترې",
"alertDialogLabel": "خبرتیا"
"alertDialogLabel": "خبرتیا",
"searchFieldLabel": "لټون"
}
......@@ -40,5 +40,6 @@
"drawerLabel": "Menu de navegação",
"popupMenuLabel": "Menu pop-up",
"dialogLabel": "Caixa de diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Pesquisa"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menu de navegação",
"popupMenuLabel": "Menu pop-up",
"dialogLabel": "Caixa de diálogo",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Pesquisa"
}
......@@ -40,5 +40,6 @@
"drawerLabel": "Meniu de navigare",
"popupMenuLabel": "Meniu pop-up",
"dialogLabel": "Casetă de dialog",
"alertDialogLabel": "Alerta"
"alertDialogLabel": "Alerta",
"searchFieldLabel": "Căutare"
}
......@@ -41,5 +41,6 @@
"drawerLabel": "Меню навигации",
"popupMenuLabel": "Всплывающее меню",
"dialogLabel": "Диалоговое окно",
"alertDialogLabel": "бдительный"
"alertDialogLabel": "бдительный",
"searchFieldLabel": "Поиск"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "เมนูการนำทาง",
"popupMenuLabel": "เมนูป๊อปอัป",
"dialogLabel": "กล่องโต้ตอบ",
"alertDialogLabel": "เตือนภัย"
"alertDialogLabel": "เตือนภัย",
"searchFieldLabel": "ค้นหา"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Gezinme menüsü",
"popupMenuLabel": "Popup menü",
"dialogLabel": "İletişim kutusu",
"alertDialogLabel": "Alarm"
"alertDialogLabel": "Alarm",
"searchFieldLabel": "Arama"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "نیویگیشن مینو",
"popupMenuLabel": "پاپ اپ مینو",
"dialogLabel": "ڈائیلاگ",
"alertDialogLabel": "انتباہ"
"alertDialogLabel": "انتباہ",
"searchFieldLabel": "تلاش کریں"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "Menu di chuyển",
"popupMenuLabel": "Menu bật lên",
"dialogLabel": "Hộp thoại",
"alertDialogLabel": "Hộp thoại"
"alertDialogLabel": "Hộp thoại",
"searchFieldLabel": "Tìm kiếm"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "导航菜单",
"popupMenuLabel": "弹出菜单",
"dialogLabel": "对话框",
"alertDialogLabel": "警报"
"alertDialogLabel": "警报",
"searchFieldLabel": "搜索"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "導覽選單",
"popupMenuLabel": "彈出式選單",
"dialogLabel": "對話方塊",
"alertDialogLabel": "警报"
"alertDialogLabel": "警报",
"searchFieldLabel": "搜索"
}
......@@ -38,5 +38,6 @@
"drawerLabel": "導覽選單",
"popupMenuLabel": "彈出式選單",
"dialogLabel": "對話方塊",
"alertDialogLabel": "警报"
"alertDialogLabel": "警报",
"searchFieldLabel": "搜索"
}
......@@ -16,7 +16,7 @@ import 'widgets_localizations.dart';
// Watch out: the supported locales list in the doc comment below must be kept
// in sync with the list we test, see test/translations_test.dart, and of course
// the acutal list of supported locales in _MaterialLocalizationsDelegate.
// the actual list of supported locales in _MaterialLocalizationsDelegate.
/// Localized strings for the material widgets.
///
......@@ -268,6 +268,9 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
@override
String get alertDialogLabel => _translationBundle.alertDialogLabel;
@override
String get searchFieldLabel => _translationBundle.searchFieldLabel;
@override
String aboutListTileTitle(String applicationName) {
final String text = _translationBundle.aboutListTileTitle;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment