diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index b57e0542..21c24668 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -57,6 +57,7 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart import 'package:weblibre/presentation/hooks/cached_future.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; +import 'package:weblibre/presentation/widgets/non_focusable.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; @@ -325,78 +326,90 @@ class BrowserTabBar extends HookConsumerWidget { return Visibility( visible: tabBarReaderView || readerabilityStateActive, - child: ReaderButton( - buttonBuilder: (isLoading, readerActive, icon) => - InkWell( - onTap: isLoading - ? null - : () async { - await ref - .read( - readerableScreenControllerProvider - .notifier, - ) - .toggleReaderView(!readerActive); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, + child: NonFocusable( + child: ReaderButton( + buttonBuilder: (isLoading, readerActive, icon) => + InkWell( + onTap: isLoading + ? null + : () async { + await ref + .read( + readerableScreenControllerProvider + .notifier, + ) + .toggleReaderView( + !readerActive, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: icon, ), - child: icon, ), - ), + ), ), ); }, ), if (showExtensionShortcut) - ExtensionShortcutMenu( - controller: extensionMenuController, - child: IconButton( - onPressed: () { - if (extensionMenuController.isOpen) { - extensionMenuController.close(); - } else { - extensionMenuController.open(); - } - }, - icon: const Icon(MdiIcons.puzzle), + NonFocusable( + child: ExtensionShortcutMenu( + controller: extensionMenuController, + child: IconButton( + onPressed: () { + if (extensionMenuController.isOpen) { + extensionMenuController.close(); + } else { + extensionMenuController.open(); + } + }, + icon: const Icon(MdiIcons.puzzle), + ), ), ), if (selectedTabId != null) - TabMenu( - controller: trippleDotMenuController, - selectedTabId: selectedTabId, - builder: (context, controller, child) { - return InkWell( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Padding( - padding: EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 15.0, + NonFocusable( + child: TabMenu( + controller: trippleDotMenuController, + selectedTabId: selectedTabId, + builder: (context, controller, child) { + return InkWell( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 15.0, + ), + child: Icon(MdiIcons.dotsVertical), ), - child: Icon(MdiIcons.dotsVertical), - ), - ); - }, + ); + }, + ), ), if (showMainToolbarTabsCount) - TabsCountButton( - selectedTabId: selectedTabId, - displayedSheet: displayedSheet, - showLongPressMenu: true, + NonFocusable( + child: TabsCountButton( + selectedTabId: selectedTabId, + displayedSheet: displayedSheet, + showLongPressMenu: true, + ), ), if (showMainToolbarNavigationButton) - NavigationMenuButton( - selectedTabId: selectedTabId, - showNavigationButtons: true, + NonFocusable( + child: NavigationMenuButton( + selectedTabId: selectedTabId, + showNavigationButtons: true, + ), ), ], ), @@ -440,32 +453,42 @@ class ContextualToolbar extends HookConsumerWidget { children: [ if (tabState?.historyState.canGoBack == true || tabState?.isLoading == true) - NavigateBackButton( - selectedTabId: selectedTabId, - isLoading: tabState?.isLoading ?? false, + NonFocusable( + child: NavigateBackButton( + selectedTabId: selectedTabId, + isLoading: tabState?.isLoading ?? false, + ), ) else - IconButton( - onPressed: () async { - await BookmarkListRoute( - entryGuid: BookmarkRoot.root.id, - ).push(context); - }, - icon: const Icon(MdiIcons.bookmarkMultiple), + NonFocusable( + child: IconButton( + onPressed: () async { + await BookmarkListRoute( + entryGuid: BookmarkRoot.root.id, + ).push(context); + }, + icon: const Icon(MdiIcons.bookmarkMultiple), + ), ), if (tabState?.historyState.canGoForward == true) - NavigateForwardButton(selectedTabId: selectedTabId) + NonFocusable( + child: NavigateForwardButton(selectedTabId: selectedTabId), + ) else - ShareMenuButton(selectedTabId: selectedTabId), - const AddTabButton(), - TabsCountButton( - selectedTabId: selectedTabId, - displayedSheet: displayedSheet, - showLongPressMenu: false, + NonFocusable(child: ShareMenuButton(selectedTabId: selectedTabId)), + const NonFocusable(child: AddTabButton()), + NonFocusable( + child: TabsCountButton( + selectedTabId: selectedTabId, + displayedSheet: displayedSheet, + showLongPressMenu: false, + ), ), - NavigationMenuButton( - selectedTabId: selectedTabId, - showNavigationButtons: false, + NonFocusable( + child: NavigationMenuButton( + selectedTabId: selectedTabId, + showNavigationButtons: false, + ), ), ], ); @@ -628,15 +651,17 @@ class ShareMenuButton extends HookConsumerWidget { ShowQrCodeMenuItemButton(selectedTabId: selectedTabId), ], builder: (context, controller, child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.share), + return NonFocusable( + child: IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.share), + ), ); }, ); @@ -705,17 +730,19 @@ class NavigationMenuButton extends HookConsumerWidget { alignment: WrapAlignment.center, children: [ ...pageExtensions.map( - (extension) => IconButton( - onPressed: () async { - //Use parents .ref because after onPressed this consumer gets disposed already - await addonService.invokeAddonAction( - extension.extensionId, - WebExtensionActionType.page, - ); - }, - icon: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: ExtensionBadgeIcon(extension), + (extension) => NonFocusable( + child: IconButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await addonService.invokeAddonAction( + extension.extensionId, + WebExtensionActionType.page, + ); + }, + icon: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: ExtensionBadgeIcon(extension), + ), ), ), ), @@ -947,18 +974,20 @@ class NavigateForwardButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return IconButton( - onPressed: canGoForward - ? () async { - final controller = ref.read( - tabSessionProvider(tabId: selectedTabId).notifier, - ); + return NonFocusable( + child: IconButton( + onPressed: canGoForward + ? () async { + final controller = ref.read( + tabSessionProvider(tabId: selectedTabId).notifier, + ); - await controller.goForward(); - menuControllerToClose?.close(); - } - : null, - icon: const Icon(Icons.arrow_forward), + await controller.goForward(); + menuControllerToClose?.close(); + } + : null, + icon: const Icon(Icons.arrow_forward), + ), ); } } @@ -979,33 +1008,37 @@ class NavigateBackButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return IconButton( - onPressed: (canGoBack || isLoading) - ? () async { - final controller = ref.read( - tabSessionProvider(tabId: selectedTabId).notifier, - ); + return NonFocusable( + child: IconButton( + onPressed: (canGoBack || isLoading) + ? () async { + final controller = ref.read( + tabSessionProvider(tabId: selectedTabId).notifier, + ); - final isReaderActive = ref.read( - selectedTabStateProvider.select( - (state) => state?.readerableState.active ?? false, - ), - ); + final isReaderActive = ref.read( + selectedTabStateProvider.select( + (state) => state?.readerableState.active ?? false, + ), + ); - if (isLoading) { - await controller.stopLoading(); - } else if (isReaderActive) { - await ref - .read(readerableScreenControllerProvider.notifier) - .toggleReaderView(false); - } else { - await controller.goBack(); + if (isLoading) { + await controller.stopLoading(); + } else if (isReaderActive) { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(false); + } else { + await controller.goBack(); + } + + menuControllerToClose?.close(); } - - menuControllerToClose?.close(); - } - : null, - icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back), + : null, + icon: isLoading + ? const Icon(Icons.close) + : const Icon(Icons.arrow_back), + ), ); } } @@ -1019,28 +1052,30 @@ class AddTabButton extends HookConsumerWidget { return TabCreationMenu( controller: tabMenuController, - child: IconButton( - onPressed: () async { - final settings = ref.read(generalSettingsWithDefaultsProvider); + child: NonFocusable( + child: IconButton( + onPressed: () async { + final settings = ref.read(generalSettingsWithDefaultsProvider); - await SearchRoute( - tabType: - ref.read(selectedTabTypeProvider) ?? - settings.defaultCreateTabType, - ).push(context); + await SearchRoute( + tabType: + ref.read(selectedTabTypeProvider) ?? + settings.defaultCreateTabType, + ).push(context); - if (context.mounted) { - const BrowserRoute().go(context); - } - }, - icon: const Icon(MdiIcons.tabPlus), - onLongPress: () { - if (tabMenuController.isOpen) { - tabMenuController.close(); - } else { - tabMenuController.open(); - } - }, + if (context.mounted) { + const BrowserRoute().go(context); + } + }, + icon: const Icon(MdiIcons.tabPlus), + onLongPress: () { + if (tabMenuController.isOpen) { + tabMenuController.close(); + } else { + tabMenuController.open(); + } + }, + ), ), ); } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart index a80cfe09..7469b529 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart @@ -26,6 +26,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; +import 'package:weblibre/presentation/widgets/non_focusable.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; @@ -152,22 +153,24 @@ class GridTabPreview extends HookConsumerWidget { child: Text('Close all from ${tabState.url.host}'), ), ], - child: IconButton( - visualDensity: const VisualDensity( - horizontal: -4.0, - vertical: -4.0, - ), - onPressed: onDelete, - onLongPress: onDeleteAll != null - ? () { - if (extendedDeleteMenuController.isOpen) { - extendedDeleteMenuController.close(); - } else { - extendedDeleteMenuController.open(); + child: NonFocusable( + child: IconButton( + visualDensity: const VisualDensity( + horizontal: -4.0, + vertical: -4.0, + ), + onPressed: onDelete, + onLongPress: onDeleteAll != null + ? () { + if (extendedDeleteMenuController.isOpen) { + extendedDeleteMenuController.close(); + } else { + extendedDeleteMenuController.open(); + } } - } - : null, - icon: const Icon(Icons.close), + : null, + icon: const Icon(Icons.close), + ), ), ), ?trailingChild, @@ -348,18 +351,20 @@ class ListTabPreview extends HookConsumerWidget { child: Text('Close all from ${tabState.url.host}'), ), ], - child: IconButton( - onPressed: onDelete, - onLongPress: onDeleteAll != null - ? () { - if (extendedDeleteMenuController.isOpen) { - extendedDeleteMenuController.close(); - } else { - extendedDeleteMenuController.open(); + child: NonFocusable( + child: IconButton( + onPressed: onDelete, + onLongPress: onDeleteAll != null + ? () { + if (extendedDeleteMenuController.isOpen) { + extendedDeleteMenuController.close(); + } else { + extendedDeleteMenuController.open(); + } } - } - : null, - icon: const Icon(Icons.close), + : null, + icon: const Icon(Icons.close), + ), ), ), ?trailingChild, @@ -625,10 +630,12 @@ class SuggestedSingleGridTabPreview extends StatelessWidget { tabId: tabId, isActive: tabId == activeTabId, onTap: onTap, - trailingChild: const IconButton( - visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0), - icon: Icon(MdiIcons.creation), - onPressed: null, + trailingChild: const NonFocusable( + child: IconButton( + visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0), + icon: Icon(MdiIcons.creation), + onPressed: null, + ), ), ), ); @@ -657,10 +664,12 @@ class SuggestedSingleListTabPreview extends StatelessWidget { tabId: tabId, isActive: tabId == activeTabId, onTap: onTap, - trailingChild: const IconButton( - visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0), - icon: Icon(MdiIcons.creation), - onPressed: null, + trailingChild: const NonFocusable( + child: IconButton( + visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0), + icon: Icon(MdiIcons.creation), + onPressed: null, + ), ), ), ); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart index f22f86ed..822bd4d9 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart @@ -26,6 +26,7 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/c import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; +import 'package:weblibre/presentation/widgets/non_focusable.dart'; import 'package:weblibre/presentation/widgets/speech_to_text_button.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; @@ -82,27 +83,31 @@ class TabViewHeader extends HookConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(MdiIcons.tabSearch), - iconSize: 18, - padding: EdgeInsets.zero, - tooltip: 'Search inside tabs', - onPressed: () { - switch (ref.read(tabsViewModeControllerProvider)) { - case TabsViewMode.tree: - case TabsViewMode.list: - break; - case TabsViewMode.grid: - ref - .read( - tabsViewModeControllerProvider.notifier, - ) - .set(TabsViewMode.list); - } + NonFocusable( + child: IconButton( + icon: const Icon(MdiIcons.tabSearch), + iconSize: 18, + padding: EdgeInsets.zero, + tooltip: 'Search inside tabs', + onPressed: () { + switch (ref.read( + tabsViewModeControllerProvider, + )) { + case TabsViewMode.tree: + case TabsViewMode.list: + break; + case TabsViewMode.grid: + ref + .read( + tabsViewModeControllerProvider.notifier, + ) + .set(TabsViewMode.list); + } - searchMode.value = true; - searchTextFocus.requestFocus(); - }, + searchMode.value = true; + searchTextFocus.requestFocus(); + }, + ), ), const SizedBox( height: 32, @@ -126,21 +131,23 @@ class TabViewHeader extends HookConsumerWidget { ), ) .toList(), - child: IconButton( - tooltip: 'Change view mode', - onPressed: () { - if (viewModeMenuController.isOpen) { - viewModeMenuController.close(); - } else { - viewModeMenuController.open(); - } - }, - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(tabsViewMode.icon, size: 18), - const Icon(Icons.arrow_drop_down, size: 18), - ], + child: NonFocusable( + child: IconButton( + tooltip: 'Change view mode', + onPressed: () { + if (viewModeMenuController.isOpen) { + viewModeMenuController.close(); + } else { + viewModeMenuController.open(); + } + }, + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(tabsViewMode.icon, size: 18), + const Icon(Icons.arrow_drop_down, size: 18), + ], + ), ), ), ), @@ -167,40 +174,42 @@ class TabViewHeader extends HookConsumerWidget { style: const TextStyle(fontSize: 10), ) : null, - child: IconButton.filledTonal( - icon: const Icon(MdiIcons.imageAutoAdjust), - isSelected: tabSuggestionsEnabled, - iconSize: 18, - padding: EdgeInsets.zero, - tooltip: downloadProgress != null - ? 'Downloading AI models (${downloadProgress.progress.toInt()}%)' - : tabSuggestionsEnabled - ? 'Disable AI tab suggestions' - : 'Enable AI tab suggestions', - onPressed: () async { - if (!tabSuggestionsEnabled) { - final result = - await showEnableAiTabSuggestionsDialog( - context, - ); + child: NonFocusable( + child: IconButton.filledTonal( + icon: const Icon(MdiIcons.imageAutoAdjust), + isSelected: tabSuggestionsEnabled, + iconSize: 18, + padding: EdgeInsets.zero, + tooltip: downloadProgress != null + ? 'Downloading AI models (${downloadProgress.progress.toInt()}%)' + : tabSuggestionsEnabled + ? 'Disable AI tab suggestions' + : 'Enable AI tab suggestions', + onPressed: () async { + if (!tabSuggestionsEnabled) { + final result = + await showEnableAiTabSuggestionsDialog( + context, + ); - if (result == true) { + if (result == true) { + ref + .read( + tabSuggestionsControllerProvider + .notifier, + ) + .enable(); + } + } else { ref .read( tabSuggestionsControllerProvider .notifier, ) - .enable(); + .disable(); } - } else { - ref - .read( - tabSuggestionsControllerProvider - .notifier, - ) - .disable(); - } - }, + }, + ), ), ); }, @@ -504,16 +513,18 @@ class TabViewHeader extends HookConsumerWidget { }, ), ], - child: IconButton( - tooltip: 'Tab actions', - onPressed: () { - if (tabsActionMenuController.isOpen) { - tabsActionMenuController.close(); - } else { - tabsActionMenuController.open(); - } - }, - icon: const Icon(MdiIcons.dotsVertical), + child: NonFocusable( + child: IconButton( + tooltip: 'Tab actions', + onPressed: () { + if (tabsActionMenuController.isOpen) { + tabsActionMenuController.close(); + } else { + tabsActionMenuController.open(); + } + }, + icon: const Icon(MdiIcons.dotsVertical), + ), ), ), ], @@ -537,13 +548,15 @@ class TabViewHeader extends HookConsumerWidget { searchTextController.text = data; }, ), - IconButton( - onPressed: () { - searchTextController.clear(); - searchTextFocus.requestFocus(); - searchMode.value = false; - }, - icon: const Icon(Icons.clear), + NonFocusable( + child: IconButton( + onPressed: () { + searchTextController.clear(); + searchTextFocus.requestFocus(); + searchMode.value = false; + }, + icon: const Icon(Icons.clear), + ), ), ], ), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart index 67fb8ab3..f3df1a23 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart @@ -26,6 +26,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/containe import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; +import 'package:weblibre/presentation/widgets/non_focusable.dart'; class TabsActionButton extends HookConsumerWidget { final bool isActive; @@ -56,63 +57,65 @@ class TabsActionButton extends HookConsumerWidget { selectedContainerDataProvider.select((value) => value.value?.color), ); - return InkWell( - onTap: onTap, - onDoubleTap: onDoubleTap, - onLongPress: onLongPress, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 15.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - width: 2.0, - color: isActive - ? theme.colorScheme.primary - : DefaultTextStyle.of(context).style.color!, + return NonFocusable( + child: InkWell( + onTap: onTap, + onDoubleTap: onDoubleTap, + onLongPress: onLongPress, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 15.0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.0, + color: isActive + ? theme.colorScheme.primary + : DefaultTextStyle.of(context).style.color!, + ), + borderRadius: BorderRadius.circular(5.0), + color: containerColor.mapNotNull(ContainerColors.forAppBar), ), - borderRadius: BorderRadius.circular(5.0), - color: containerColor.mapNotNull(ContainerColors.forAppBar), - ), - constraints: const BoxConstraints(minWidth: 25.0), - child: Center( - child: tabCount.when( - skipLoadingOnReload: true, - data: (count) { - return Text( - count.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: isActive ? theme.colorScheme.primary : null, - ), - ); - }, - loading: () => (tabCount.hasValue) - ? Text( - tabCount.value.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: isActive ? theme.colorScheme.primary : null, - ), - ) - : const Skeletonizer(child: Text('00')), - error: (error, stackTrace) { - logger.e( - 'Could not determine tab count', - error: error, - stackTrace: stackTrace, - ); + constraints: const BoxConstraints(minWidth: 25.0), + child: Center( + child: tabCount.when( + skipLoadingOnReload: true, + data: (count) { + return Text( + count.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: isActive ? theme.colorScheme.primary : null, + ), + ); + }, + loading: () => (tabCount.hasValue) + ? Text( + tabCount.value.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: isActive ? theme.colorScheme.primary : null, + ), + ) + : const Skeletonizer(child: Text('00')), + error: (error, stackTrace) { + logger.e( + 'Could not determine tab count', + error: error, + stackTrace: stackTrace, + ); - return Text( - '-1', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: isActive ? theme.colorScheme.primary : null, - ), - ); - }, + return Text( + '-1', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: isActive ? theme.colorScheme.primary : null, + ), + ); + }, + ), ), ), ), diff --git a/app/lib/presentation/widgets/non_focusable.dart b/app/lib/presentation/widgets/non_focusable.dart new file mode 100644 index 00000000..8967e591 --- /dev/null +++ b/app/lib/presentation/widgets/non_focusable.dart @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +/// A wrapper that prevents a widget from receiving keyboard focus. +/// +/// This fixes the issue where arrow keys on a bluetooth keyboard navigate +/// to toolbar buttons instead of moving the text cursor in input fields. +/// +/// Example: +/// ```dart +/// NonFocusable( +/// child: IconButton( +/// onPressed: () {}, +/// icon: Icon(Icons.menu), +/// ), +/// ) +/// ``` +class NonFocusable extends StatelessWidget { + final Widget child; + + const NonFocusable({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + return Focus(canRequestFocus: false, skipTraversal: true, child: child); + } +}