diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 3e4d0fb4..41f6178c 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -47,177 +47,164 @@ class HomeScreen extends View with PromptHandler { actionText: request.actionLabel, actionCallback: request.action, ), - child: Stack( - children: [ - Semantics( - label: viewModel.hasQuestionnaire - ? appLocale.semanticsReturnToMap - : appLocale.semanticsFlutterMap, - child: FlutterMap( - mapController: viewModel.mapController, - options: MapOptions( - onTap: (_, __) => viewModel.closeQuestionnaire(), - interactionOptions: const InteractionOptions( - enableMultiFingerGestureRace: true, - ), - initialCenter: untracked(() => viewModel.storedMapLocation), - initialZoom: untracked(() => viewModel.storedMapZoom), - initialRotation: untracked(() => viewModel.storedMapRotation), - minZoom: kTileLayerPublicTransport.minZoom.toDouble(), - maxZoom: kTileLayerPublicTransport.maxZoom.toDouble(), - backgroundColor: Theme.of(context).colorScheme.background, - ), - children: [ - TileLayer( - tileProvider: NetworkTileProvider( - headers: { - 'User-Agent': appUserAgent, - }, - ), - retinaMode: RetinaMode.isHighDensity(context), - evictErrorTileStrategy: EvictErrorTileStrategy.dispose, - urlTemplate: isDarkMode && kTileLayerPublicTransport.darkVariantTemplateUrl != null - ? kTileLayerPublicTransport.darkVariantTemplateUrl - : kTileLayerPublicTransport.templateUrl, - minNativeZoom: kTileLayerPublicTransport.minZoom, - maxNativeZoom: kTileLayerPublicTransport.maxZoom, - ), - Observer( - builder: (context) { - // "length" used to listen to changes - viewModel.loadingStopAreas.length; - return LoadingAreaLayer( - areas: viewModel.loadingStopAreas, - ); - }, - ), - Observer( - builder: (context) { - // "length" used to listen to changes - viewModel.completeStopAreas.length; - return CompletedAreaLayer( - currentZoom: viewModel.mapZoomRound, - locations: viewModel.completeStopAreas.map((s) => s.stops.first.location), - ); - }, - ), - Observer( - builder: (context) { - // "length" used to listen to changes - viewModel - ..unloadedStopAreas.length - ..incompleteStopAreas.length - ..completeStopAreas.length; - - return StopsLayer( - currentZoom: viewModel.mapZoomRound, - unloadedStops: viewModel.unloadedStopAreas.map((s) => s.stops.first.location), - incompleteStops: viewModel.incompleteStopAreas.map((s) => s.stops.first.location), - completedStops: viewModel.completeStopAreas.map((s) => s.stops.first.location), - ); - }, - ), - Observer( - builder: (context) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: (viewModel.hasSelectedElement) - ? GeometryLayer( - geometry: viewModel.selectedElement!.geometry, - key: viewModel.selectedElementKey, - ) - : null, - ); - }, - ), - Observer( - builder: (context) { - // rebuild location indicator when location access is granted - viewModel.userLocationState; - return const AnimatedLocationLayer(); - }, - ), - Observer( - builder: (context) { - return OsmElementLayer( - elements: viewModel.elements, - currentZoom: viewModel.mapZoomRound, - onOsmElementTap: viewModel.onElementTap, - selectedElement: viewModel.selectedElement, - ); - }, - ), - RepaintBoundary( - child: AnimatedSwitcher( - switchInCurve: Curves.ease, - switchOutCurve: Curves.ease, - duration: const Duration(milliseconds: 300), - child: !viewModel.hasQuestionnaire - ? const MapOverlay() - : null, - ), - ), - ], + child: Semantics( + container: true, + sortKey: const OrdinalSortKey(2.0, name: 'mapLayer'), + label: viewModel.hasQuestionnaire + ? appLocale.semanticsReturnToMap + : appLocale.semanticsFlutterMap, + child: FlutterMap( + mapController: viewModel.mapController, + options: MapOptions( + onTap: (_, __) => viewModel.closeQuestionnaire(), + interactionOptions: const InteractionOptions( + enableMultiFingerGestureRace: true, ), + initialCenter: untracked(() => viewModel.storedMapLocation), + initialZoom: untracked(() => viewModel.storedMapZoom), + initialRotation: untracked(() => viewModel.storedMapRotation), + minZoom: kTileLayerPublicTransport.minZoom.toDouble(), + maxZoom: kTileLayerPublicTransport.maxZoom.toDouble(), + backgroundColor: Theme.of(context).colorScheme.background, ), - // place sheet on extra stack above map so map pan events won't pass through - Observer( - builder: (context) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOutCubicEmphasized, - switchOutCurve: Curves.ease, - transitionBuilder: (child, animation) { - final offsetAnimation = Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(animation); - return SlideTransition( - position: offsetAnimation, - child: FadeTransition( - opacity: animation, - child: child, - ) - ); + children: [ + TileLayer( + tileProvider: NetworkTileProvider( + headers: { + 'User-Agent': appUserAgent, }, - child: viewModel.hasQuestionnaire - ? BlockSemantics( - child: Stack( - children: [ - Semantics( - sortKey: const OrdinalSortKey(2.0, name: 'QuestionSheet'), - child: Visibility( - visible: false, - maintainState: true, - maintainSemantics: true, - maintainSize: true, - maintainAnimation: true, - maintainInteractivity: true, - child: ModalBarrier( - onDismiss: () => viewModel.closeQuestionnaire(), - semanticsLabel: appLocale.semanticsCloseNavigationMenuButton, - ), - ), - ), - Semantics( - sortKey: const OrdinalSortKey(1.0, name: 'QuestionSheet'), - child: QuestionDialog( + ), + retinaMode: RetinaMode.isHighDensity(context), + evictErrorTileStrategy: EvictErrorTileStrategy.dispose, + urlTemplate: isDarkMode && kTileLayerPublicTransport.darkVariantTemplateUrl != null + ? kTileLayerPublicTransport.darkVariantTemplateUrl + : kTileLayerPublicTransport.templateUrl, + minNativeZoom: kTileLayerPublicTransport.minZoom, + maxNativeZoom: kTileLayerPublicTransport.maxZoom, + ), + Observer( + builder: (context) { + // "length" used to listen to changes + viewModel.loadingStopAreas.length; + return LoadingAreaLayer( + areas: viewModel.loadingStopAreas, + ); + }, + ), + Observer( + builder: (context) { + // "length" used to listen to changes + viewModel.completeStopAreas.length; + return CompletedAreaLayer( + currentZoom: viewModel.mapZoomRound, + locations: viewModel.completeStopAreas.map((s) => s.stops.first.location), + ); + }, + ), + Observer( + builder: (context) { + // "length" used to listen to changes + viewModel + ..unloadedStopAreas.length + ..incompleteStopAreas.length + ..completeStopAreas.length; - activeQuestionIndex: viewModel.currentQuestionnaireIndex!, - questions: viewModel.questionnaireQuestions, - answers: viewModel.questionnaireAnswers, - showSummary: viewModel.questionnaireIsFinished, - key: viewModel.selectedElementKey, - ),), - ] - ), + return StopsLayer( + currentZoom: viewModel.mapZoomRound, + unloadedStops: viewModel.unloadedStopAreas.map((s) => s.stops.first.location), + incompleteStops: viewModel.incompleteStopAreas.map((s) => s.stops.first.location), + completedStops: viewModel.completeStopAreas.map((s) => s.stops.first.location), + ); + }, + ), + Observer( + builder: (context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: (viewModel.hasSelectedElement) + ? GeometryLayer( + geometry: viewModel.selectedElement!.geometry, + key: viewModel.selectedElementKey, ) - : null - ); - }, - ), - ], + : null, + ); + }, + ), + Observer( + builder: (context) { + // rebuild location indicator when location access is granted + viewModel.userLocationState; + return const AnimatedLocationLayer(); + }, + ), + Semantics( + container: true, + sortKey: const OrdinalSortKey(1.0, name: 'mapLayer'), + child: BlockSemantics( + blocking: viewModel.hasQuestionnaire, + child: Stack( + children: [ + // PERHAPS REQUIRES RE-ORDERING + Observer( + builder: (context) { + return OsmElementLayer( + elements: viewModel.elements, + currentZoom: viewModel.mapZoomRound, + onOsmElementTap: viewModel.onElementTap, + selectedElement: viewModel.selectedElement, + ); + }, + ), + RepaintBoundary( + child: AnimatedSwitcher( + switchInCurve: Curves.ease, + switchOutCurve: Curves.ease, + duration: const Duration(milliseconds: 300), + child: !viewModel.hasQuestionnaire + ? const MapOverlay() + : null, + ), + ), + // WILL PROBABLY REQUIRE ABSORBING POINTERS to solve comment below + // place sheet on extra stack above map so map pan events won't pass through + Observer( + builder: (context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + reverseDuration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOutCubicEmphasized, + switchOutCurve: Curves.ease, + transitionBuilder: (child, animation) { + final offsetAnimation = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation); + return SlideTransition( + position: offsetAnimation, + child: FadeTransition( + opacity: animation, + child: child, + ) + ); + }, + child: viewModel.hasQuestionnaire + ? QuestionDialog( + activeQuestionIndex: viewModel.currentQuestionnaireIndex!, + questions: viewModel.questionnaireQuestions, + answers: viewModel.questionnaireAnswers, + showSummary: viewModel.questionnaireIsFinished, + key: viewModel.selectedElementKey, + ) + : null + ); + }, + ), + ] + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/widgets/home_sidebar/home_sidebar.dart b/lib/widgets/home_sidebar/home_sidebar.dart index eec8a531..b3faabd3 100644 --- a/lib/widgets/home_sidebar/home_sidebar.dart +++ b/lib/widgets/home_sidebar/home_sidebar.dart @@ -60,19 +60,6 @@ class HomeSidebar extends ViewFragment { title: appLocale.helpTitle, onTap: () => Navigator.push(context, Routes.help), ), - Visibility( - visible: false, - maintainState: true, - maintainSemantics: true, - maintainSize: true, - maintainAnimation: true, - maintainInteractivity: true, - child: CustomListTile( - leadingIcon: Icons.arrow_back, - title: appLocale.semanticsCloseNavigationMenuButton, - onTap: () => Navigator.pop(context), - ), - ), ], ), ); diff --git a/lib/widgets/map_overlay/zoom_button.dart b/lib/widgets/map_overlay/zoom_button.dart index da3b2068..dd69344a 100644 --- a/lib/widgets/map_overlay/zoom_button.dart +++ b/lib/widgets/map_overlay/zoom_button.dart @@ -26,8 +26,6 @@ class ZoomButton extends StatelessWidget { child: Column( children: [ Semantics( - container: true, - sortKey: const OrdinalSortKey(1.0, name: 'ZoomButton'), child: SizedBox( height: (Theme.of(context).floatingActionButtonTheme.smallSizeConstraints?.minHeight ?? 48.0) * 1.25, width: Theme.of(context).floatingActionButtonTheme.smallSizeConstraints?.minWidth ?? 48.0, @@ -47,8 +45,6 @@ class ZoomButton extends StatelessWidget { color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1), ), Semantics( - container: true, - sortKey: const OrdinalSortKey(2.0, name: 'ZoomButton'), child: SizedBox( height: (Theme.of(context).floatingActionButtonTheme.smallSizeConstraints?.minHeight ?? 48.0) * 1.25, width: Theme.of(context).floatingActionButtonTheme.smallSizeConstraints?.minWidth ?? 48.0, diff --git a/lib/widgets/osm_element_layer/osm_element_marker.dart b/lib/widgets/osm_element_layer/osm_element_marker.dart index 16401994..579dbc50 100644 --- a/lib/widgets/osm_element_layer/osm_element_marker.dart +++ b/lib/widgets/osm_element_layer/osm_element_marker.dart @@ -61,9 +61,9 @@ class _OsmElementMarkerState extends State with SingleTickerPr // add repaint boundary for performance improvement // this way a marker will only be redrawn if itself changes return Semantics( - label: widget.label, excludeSemantics: widget.active ? true : false, blockUserActions: widget.active ? true : false, + label: widget.active ? null : widget.label, child: RepaintBoundary( child: Center( child: GestureDetector( diff --git a/lib/widgets/question_dialog/question_navigation_bar.dart b/lib/widgets/question_dialog/question_navigation_bar.dart index 382e045b..1ec45d89 100644 --- a/lib/widgets/question_dialog/question_navigation_bar.dart +++ b/lib/widgets/question_dialog/question_navigation_bar.dart @@ -70,27 +70,29 @@ class QuestionNavigationBar extends StatelessWidget { child: nextText == null ? null : Semantics( - container: true, - button: true, - enabled: onNext != null, - sortKey: const OrdinalSortKey(1.0, name: 'questionDialog'), - child: TextButton( - key: ValueKey(nextText), - // mimic disabled style - style: onNext != null - ? _buttonStyle - : _buttonStyle.merge(disabledButtonStyle), - // if button is disabled vibrate when pressed as additional feedback - onPressed: onNext ?? HapticFeedback.vibrate, - isSemanticButton: true, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(nextText!, semanticsLabel: nextTextSemantics,), - const Icon(Icons.chevron_right_rounded), - ], + container: true, + sortKey: const OrdinalSortKey(1.0, name: 'questionDialog'), + child: TextButton( + key: ValueKey(nextText), + // mimic disabled style + style: onNext != null + ? _buttonStyle + : _buttonStyle.merge(disabledButtonStyle), + // if button is disabled vibrate when pressed as additional feedback + onPressed: onNext ?? HapticFeedback.vibrate, + isSemanticButton: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + button: true, + enabled: onNext != null, + child: Text(nextText!, semanticsLabel: nextTextSemantics,), + ), + const Icon(Icons.chevron_right_rounded), + ], + ), ), - ), ), ), ), diff --git a/lib/widgets/question_inputs/bool_input.dart b/lib/widgets/question_inputs/bool_input.dart index 50caa0b8..7300dd2f 100644 --- a/lib/widgets/question_inputs/bool_input.dart +++ b/lib/widgets/question_inputs/bool_input.dart @@ -31,17 +31,19 @@ class BoolInput extends QuestionInputWidget { child:_BoolInputItem( label: Semantics( container: true, + checked: controller.answer?.value == state, selected: controller.answer?.value == state, child: Text(definition.input[index].name ?? (state ? appLocale.yes : appLocale.no)), ), - onTap: () => _handleChange(state, appLocale), - active: controller.answer?.value == state, - backgroundColor: theme.colorScheme.primary.withOpacity(0), - activeBackgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.primary, - activeForegroundColor: theme.colorScheme.onPrimary, - ),); + onTap: () => _handleChange(state, appLocale), + active: controller.answer?.value == state, + backgroundColor: theme.colorScheme.primary.withOpacity(0), + activeBackgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.primary, + activeForegroundColor: theme.colorScheme.onPrimary, + ), + ); }, growable: false), ), ); diff --git a/lib/widgets/question_inputs/list_input.dart b/lib/widgets/question_inputs/list_input.dart index cef33001..6a66ec56 100644 --- a/lib/widgets/question_inputs/list_input.dart +++ b/lib/widgets/question_inputs/list_input.dart @@ -26,6 +26,7 @@ class ListInput extends QuestionInputWidget { description: item.description, imagePath: item.image, onTap: () => _handleChange(index, appLocale), + isMultiList: false, ); }, growable: false), ); @@ -49,10 +50,12 @@ class ListInputItem extends StatefulWidget { final double imagePadding; final bool active; final VoidCallback onTap; + final bool isMultiList; const ListInputItem({ required this.label, required this.onTap, + required this.isMultiList, this.active = false, this.description, this.imagePath, @@ -123,7 +126,9 @@ class _ListInputItemState extends State with SingleTickerProvider ), child: Semantics( container: true, - selected: widget.active, + inMutuallyExclusiveGroup: widget.isMultiList ? true : null, + checked: widget.active, + selected: widget.isMultiList ? widget.active : null, child: Text( semanticsLabel: '${widget.label} ${widget.description ?? ''}', widget.label, diff --git a/lib/widgets/question_inputs/multi_list_input.dart b/lib/widgets/question_inputs/multi_list_input.dart index cba54cc6..bd8f9d87 100644 --- a/lib/widgets/question_inputs/multi_list_input.dart +++ b/lib/widgets/question_inputs/multi_list_input.dart @@ -27,6 +27,7 @@ class MultiListInput extends QuestionInputWidget _handleChange(index, appLocale), + isMultiList: true, ); }, growable: false), );