Bug 1879377 - Hide nav bar when the toolbar becomes focused

fenix/125.0
mike a 3 months ago committed by mergify[bot]
parent ebfc9edc79
commit 1d795b4372

@ -60,7 +60,6 @@ import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
@ -210,7 +209,9 @@ abstract class BaseBrowserFragment :
internal val browserToolbarView: BrowserToolbarView internal val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!! get() = _browserToolbarView!!
internal lateinit var browserToolbar: BrowserToolbar private var _bottomToolbarContainerView: BottomToolbarContainerView? = null
private val bottomToolbarContainerView: BottomToolbarContainerView
get() = _bottomToolbarContainerView!!
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>() protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>() protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
@ -441,9 +442,7 @@ abstract class BaseBrowserFragment :
interactor = browserToolbarInteractor, interactor = browserToolbarInteractor,
customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) }, customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) },
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
).also { )
browserToolbar = it.view
}
if (IncompleteRedesignToolbarFeature(context.settings()).isEnabled) { if (IncompleteRedesignToolbarFeature(context.settings()).isEnabled) {
val isToolbarAtBottom = context.components.settings.toolbarPosition == ToolbarPosition.BOTTOM val isToolbarAtBottom = context.components.settings.toolbarPosition == ToolbarPosition.BOTTOM
@ -452,6 +451,7 @@ abstract class BaseBrowserFragment :
// We should remove it and add the view to the navigation bar container. // We should remove it and add the view to the navigation bar container.
// Should refactor this so there is no added view to remove to begin with: // Should refactor this so there is no added view to remove to begin with:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1870976 // https://bugzilla.mozilla.org/show_bug.cgi?id=1870976
val browserToolbar = browserToolbarView.view
if (isToolbarAtBottom) { if (isToolbarAtBottom) {
binding.browserLayout.removeView(browserToolbar) binding.browserLayout.removeView(browserToolbar)
} }
@ -467,19 +467,26 @@ abstract class BaseBrowserFragment :
) )
} }
BottomToolbarContainerView( _bottomToolbarContainerView = BottomToolbarContainerView(
context = context, context = context,
parent = binding.browserLayout, parent = binding.browserLayout,
androidToolbarView = if (isToolbarAtBottom) browserToolbar else null, androidToolbarView = if (isToolbarAtBottom) browserToolbar else null,
menuButton = menuButton, menuButton = menuButton,
isPrivateMode = activity.browsingModeManager.mode.isPrivate, isPrivateMode = activity.browsingModeManager.mode.isPrivate,
).also { )
navbarIntegration.set(
feature = it.navbarIntegration, navbarIntegration.set(
owner = this, feature = NavbarIntegration(
view = view, toolbar = bottomToolbarContainerView.toolbarContainerView,
) store = requireComponents.core.store,
} appStore = requireComponents.appStore,
viewLifecycleOwner = viewLifecycleOwner,
bottomToolbarContainerView = bottomToolbarContainerView,
sessionId = customTabSessionId,
),
owner = this,
view = view,
)
} }
toolbarIntegration.set( toolbarIntegration.set(
@ -1660,6 +1667,7 @@ abstract class BaseBrowserFragment :
binding.engineView.setActivityContext(null) binding.engineView.setActivityContext(null)
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this) requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_bottomToolbarContainerView = null
_browserToolbarView = null _browserToolbarView = null
_browserToolbarInteractor = null _browserToolbarInteractor = null
_binding = null _binding = null

@ -26,6 +26,7 @@ import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.library.history.PendingDeletionHistory import org.mozilla.fenix.library.history.PendingDeletionHistory
import org.mozilla.fenix.messaging.MessagingState import org.mozilla.fenix.messaging.MessagingState
import org.mozilla.fenix.search.SearchDialogFragment
import org.mozilla.fenix.wallpapers.Wallpaper import org.mozilla.fenix.wallpapers.Wallpaper
/** /**
@ -38,6 +39,11 @@ sealed class AppAction : Action {
* Updates whether the first frame of the homescreen has been [drawn]. * Updates whether the first frame of the homescreen has been [drawn].
*/ */
data class UpdateFirstFrameDrawn(val drawn: Boolean) : AppAction() data class UpdateFirstFrameDrawn(val drawn: Boolean) : AppAction()
/**
* Updates whether the [SearchDialogFragment] is visible.
*/
data class UpdateSearchDialogVisibility(val isVisible: Boolean) : AppAction()
data class AddNonFatalCrash(val crash: NativeCodeCrash) : AppAction() data class AddNonFatalCrash(val crash: NativeCodeCrash) : AppAction()
data class RemoveNonFatalCrash(val crash: NativeCodeCrash) : AppAction() data class RemoveNonFatalCrash(val crash: NativeCodeCrash) : AppAction()
object RemoveAllNonFatalCrashes : AppAction() object RemoveAllNonFatalCrashes : AppAction()

@ -24,6 +24,7 @@ import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.library.history.PendingDeletionHistory import org.mozilla.fenix.library.history.PendingDeletionHistory
import org.mozilla.fenix.messaging.MessagingState import org.mozilla.fenix.messaging.MessagingState
import org.mozilla.fenix.search.SearchDialogFragment
import org.mozilla.fenix.wallpapers.WallpaperState import org.mozilla.fenix.wallpapers.WallpaperState
/** /**
@ -33,6 +34,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState
* @property inactiveTabsExpanded A flag to know if the Inactive Tabs section of the Tabs Tray * @property inactiveTabsExpanded A flag to know if the Inactive Tabs section of the Tabs Tray
* should be expanded when the tray is opened. * should be expanded when the tray is opened.
* @property firstFrameDrawn Flag indicating whether the first frame of the homescreen has been drawn. * @property firstFrameDrawn Flag indicating whether the first frame of the homescreen has been drawn.
* @property isSearchDialogVisible Flag indicating whether the user is interacting with the [SearchDialogFragment].
* @property nonFatalCrashes List of non-fatal crashes that allow the app to continue being used. * @property nonFatalCrashes List of non-fatal crashes that allow the app to continue being used.
* @property collections The list of [TabCollection] to display in the [HomeFragment]. * @property collections The list of [TabCollection] to display in the [HomeFragment].
* @property expandedCollections A set containing the ids of the [TabCollection] that are expanded * @property expandedCollections A set containing the ids of the [TabCollection] that are expanded
@ -63,6 +65,7 @@ data class AppState(
val isForeground: Boolean = true, val isForeground: Boolean = true,
val inactiveTabsExpanded: Boolean = false, val inactiveTabsExpanded: Boolean = false,
val firstFrameDrawn: Boolean = false, val firstFrameDrawn: Boolean = false,
val isSearchDialogVisible: Boolean = false,
val nonFatalCrashes: List<NativeCodeCrash> = emptyList(), val nonFatalCrashes: List<NativeCodeCrash> = emptyList(),
val collections: List<TabCollection> = emptyList(), val collections: List<TabCollection> = emptyList(),
val expandedCollections: Set<Long> = emptySet(), val expandedCollections: Set<Long> = emptySet(),

@ -243,6 +243,7 @@ internal object AppStoreReducer {
is AppAction.TabStripAction.UpdateLastTabClosed -> state.copy( is AppAction.TabStripAction.UpdateLastTabClosed -> state.copy(
wasLastTabClosedPrivate = action.private, wasLastTabClosedPrivate = action.private,
) )
is AppAction.UpdateSearchDialogVisibility -> state.copy(isSearchDialogVisible = action.isVisible)
} }
} }

@ -35,7 +35,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param androidToolbarView An option toolbar view that will be added atop of the navigation bar. * @param androidToolbarView An option toolbar view that will be added atop of the navigation bar.
* @param menuButton A [MenuButton] to be used for [ItemType.MENU]. * @param menuButton A [MenuButton] to be used for [ItemType.MENU].
* @param isPrivateMode If browsing in [BrowsingMode.Private]. * @param isPrivateMode If browsing in [BrowsingMode.Private].
* @param customTabSessionId Custom tab session ID.
* *
* Defaults to [NavigationItems.defaultItems] which provides a standard set of navigation items. * Defaults to [NavigationItems.defaultItems] which provides a standard set of navigation items.
*/ */
@ -46,15 +45,13 @@ class BottomToolbarContainerView(
androidToolbarView: View? = null, androidToolbarView: View? = null,
menuButton: MenuButton, menuButton: MenuButton,
isPrivateMode: Boolean = false, isPrivateMode: Boolean = false,
customTabSessionId: String? = null,
) { ) {
private val toolbarContainerView = ToolbarContainerView(context) val toolbarContainerView = ToolbarContainerView(context)
val navbarIntegration = val composeView: ComposeView
NavbarIntegration(toolbarContainerView, parent.context.components.core.store, customTabSessionId)
init { init {
ComposeView(parent.context).apply { composeView = ComposeView(context).apply {
setContent { setContent {
val tabCount = context.components.core.store.observeAsState(initialValue = 0) { browserState -> val tabCount = context.components.core.store.observeAsState(initialValue = 0) { browserState ->
if (isPrivateMode) { if (isPrivateMode) {

@ -5,10 +5,17 @@
package org.mozilla.fenix.components.toolbar.navbar package org.mozilla.fenix.components.toolbar.navbar
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChangedBy
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.ScrollableToolbar import mozilla.components.concept.toolbar.ScrollableToolbar
import mozilla.components.feature.toolbar.ToolbarBehaviorController import mozilla.components.feature.toolbar.ToolbarBehaviorController
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.components.AppStore
/** /**
* The feature responsible for scrolling behaviour of the navigation bar. * The feature responsible for scrolling behaviour of the navigation bar.
@ -18,17 +25,29 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature
class NavbarIntegration( class NavbarIntegration(
val toolbar: ScrollableToolbar, val toolbar: ScrollableToolbar,
val store: BrowserStore, val store: BrowserStore,
val appStore: AppStore,
val viewLifecycleOwner: LifecycleOwner,
val bottomToolbarContainerView: BottomToolbarContainerView,
sessionId: String?, sessionId: String?,
) : LifecycleAwareFeature { ) : LifecycleAwareFeature {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
var toolbarController = ToolbarBehaviorController(toolbar, store, sessionId) var toolbarController = ToolbarBehaviorController(toolbar, store, sessionId)
var scope: CoroutineScope? = null
override fun start() { override fun start() {
toolbarController.start() toolbarController.start()
scope = appStore.flowScoped(viewLifecycleOwner) { flow ->
flow.distinctUntilChangedBy { it.isSearchDialogVisible }
.collect { state ->
bottomToolbarContainerView.composeView.isVisible = !state.isSearchDialogVisible
}
}
} }
override fun stop() { override fun stop() {
toolbarController.stop() toolbarController.stop()
scope?.cancel()
} }
} }

@ -66,7 +66,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment() {
feature = CustomTabsIntegration( feature = CustomTabsIntegration(
store = requireComponents.core.store, store = requireComponents.core.store,
useCases = requireComponents.useCases.customTabsUseCases, useCases = requireComponents.useCases.customTabsUseCases,
toolbar = browserToolbar, toolbar = browserToolbarView.view,
sessionId = customTabSessionId, sessionId = customTabSessionId,
activity = activity, activity = activity,
onItemTapped = { browserToolbarInteractor.onBrowserToolbarMenuItemTapped(it) }, onItemTapped = { browserToolbarInteractor.onBrowserToolbarMenuItemTapped(it) },
@ -102,7 +102,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment() {
} }
}, },
owner = this, owner = this,
view = browserToolbar, view = browserToolbarView.view,
) )
if (manifest != null) { if (manifest != null) {

@ -76,6 +76,7 @@ import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
import mozilla.components.feature.top.sites.TopSitesProviderConfig import mozilla.components.feature.top.sites.TopSitesProviderConfig
import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flow
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.ui.colors.PhotonColors import mozilla.components.ui.colors.PhotonColors
@ -95,6 +96,7 @@ import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.components.toolbar.navbar.BottomToolbarContainerView import org.mozilla.fenix.components.toolbar.navbar.BottomToolbarContainerView
import org.mozilla.fenix.components.toolbar.navbar.NavbarIntegration
import org.mozilla.fenix.databinding.FragmentHomeBinding import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.containsQueryParameters import org.mozilla.fenix.ext.containsQueryParameters
@ -156,6 +158,10 @@ class HomeFragment : Fragment() {
ToolbarPosition.TOP -> null ToolbarPosition.TOP -> null
} }
private var _bottomToolbarContainerView: BottomToolbarContainerView? = null
private val bottomToolbarContainerView: BottomToolbarContainerView
get() = _bottomToolbarContainerView!!
private val searchSelectorMenu by lazy { private val searchSelectorMenu by lazy {
SearchSelectorMenu( SearchSelectorMenu(
context = requireContext(), context = requireContext(),
@ -223,6 +229,7 @@ class HomeFragment : Fragment() {
private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>() private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
private val searchSelectorBinding = ViewBoundFeatureWrapper<SearchSelectorBinding>() private val searchSelectorBinding = ViewBoundFeatureWrapper<SearchSelectorBinding>()
private val searchSelectorMenuBinding = ViewBoundFeatureWrapper<SearchSelectorMenuBinding>() private val searchSelectorMenuBinding = ViewBoundFeatureWrapper<SearchSelectorMenuBinding>()
private val navbarIntegration = ViewBoundFeatureWrapper<NavbarIntegration>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL! // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
@ -456,13 +463,26 @@ class HomeFragment : Fragment() {
menuButton = WeakReference(menuButton), menuButton = WeakReference(menuButton),
).also { it.build() } ).also { it.build() }
BottomToolbarContainerView( _bottomToolbarContainerView = BottomToolbarContainerView(
context = requireContext(), context = requireContext(),
parent = binding.homeLayout, parent = binding.homeLayout,
androidToolbarView = if (isToolbarAtBottom) binding.toolbarLayout else null, androidToolbarView = if (isToolbarAtBottom) binding.toolbarLayout else null,
menuButton = menuButton, menuButton = menuButton,
isPrivateMode = activity.browsingModeManager.mode.isPrivate, isPrivateMode = activity.browsingModeManager.mode.isPrivate,
) )
navbarIntegration.set(
feature = NavbarIntegration(
toolbar = bottomToolbarContainerView.toolbarContainerView,
store = requireComponents.core.store,
appStore = requireComponents.appStore,
viewLifecycleOwner = viewLifecycleOwner,
bottomToolbarContainerView = bottomToolbarContainerView,
sessionId = null,
),
owner = this,
view = binding.root,
)
} }
sessionControlView = SessionControlView( sessionControlView = SessionControlView(
@ -802,6 +822,7 @@ class HomeFragment : Fragment() {
sessionControlView = null sessionControlView = null
tabCounterView = null tabCounterView = null
toolbarView = null toolbarView = null
_bottomToolbarContainerView = null
_binding = null _binding = null
bundleArgs.clear() bundleArgs.clear()

@ -82,6 +82,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.Core.Companion.BOOKMARKS_SEARCH_ENGINE_ID import org.mozilla.fenix.components.Core.Companion.BOOKMARKS_SEARCH_ENGINE_ID
import org.mozilla.fenix.components.Core.Companion.HISTORY_SEARCH_ENGINE_ID import org.mozilla.fenix.components.Core.Companion.HISTORY_SEARCH_ENGINE_ID
import org.mozilla.fenix.components.Core.Companion.TABS_SEARCH_ENGINE_ID import org.mozilla.fenix.components.Core.Companion.TABS_SEARCH_ENGINE_ID
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentSearchDialogBinding import org.mozilla.fenix.databinding.FragmentSearchDialogBinding
import org.mozilla.fenix.databinding.SearchSuggestionsHintBinding import org.mozilla.fenix.databinding.SearchSuggestionsHintBinding
@ -171,6 +172,10 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
toolbarView.view.showKeyboard() toolbarView.view.showKeyboard()
} }
} }
requireComponents.appStore.dispatch(
AppAction.UpdateSearchDialogVisibility(isVisible = true),
)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -597,6 +602,10 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
if (!dialogHandledAction) { if (!dialogHandledAction) {
requireComponents.core.store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = true)) requireComponents.core.store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = true))
} }
requireComponents.appStore.dispatch(
AppAction.UpdateSearchDialogVisibility(isVisible = false),
)
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {

@ -116,4 +116,25 @@ class AppStoreReducerTest {
assertFalse(updatedState.mode.isPrivate) assertFalse(updatedState.mode.isPrivate)
} }
@Test
fun `WHEN UpdateSearchDialogVisibility is called THEN isSearchDialogVisible gets updated`() {
val initialState = AppState()
assertFalse(initialState.isSearchDialogVisible)
var updatedState = AppStoreReducer.reduce(
initialState,
AppAction.UpdateSearchDialogVisibility(isVisible = true),
)
assertTrue(updatedState.isSearchDialogVisible)
updatedState = AppStoreReducer.reduce(
initialState,
AppAction.UpdateSearchDialogVisibility(isVisible = false),
)
assertFalse(updatedState.isSearchDialogVisible)
}
} }

@ -21,6 +21,9 @@ class NavbarIntegrationTest {
feature = NavbarIntegration( feature = NavbarIntegration(
toolbar = mockk(), toolbar = mockk(),
store = mockk(), store = mockk(),
appStore = mockk(),
viewLifecycleOwner = mockk(),
bottomToolbarContainerView = mockk(),
sessionId = null, sessionId = null,
).apply { ).apply {
toolbarController = mockk(relaxed = true) toolbarController = mockk(relaxed = true)

Loading…
Cancel
Save