commit 87a9480785754b7382c7f9800efd6ac1d47bcbbd parent fe3ae15a80a7a2d4ffb387294802b5e15722234a Author: Mugurell <Mugurell@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:06:37 +0000 Bug 1988730 - part 2 - Transition between full and minimal display toolbar when keyboard is shown r=android-reviewers,android-l10n-reviewers,flod,moyin On Android 13+ the transition will use the same duration and interpolator as the ones used for the keyboard animation to ensure a smooth animation. Differential Revision: https://phabricator.services.mozilla.com/D275990 Diffstat:
7 files changed, 594 insertions(+), 225 deletions(-)
diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserDisplayToolbar.kt @@ -4,53 +4,43 @@ package mozilla.components.compose.browser.toolbar -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.window.core.layout.WindowSizeClass -import mozilla.components.compose.base.progressbar.AnimatedProgressBar import mozilla.components.compose.base.theme.AcornTheme import mozilla.components.compose.base.theme.acornPrivateColorScheme import mozilla.components.compose.base.theme.privateColorPalette import mozilla.components.compose.browser.toolbar.concept.Action -import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes -import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction -import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction.ContentDescription.StringResContentDescription -import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction.Icon.DrawableResIcon -import mozilla.components.compose.browser.toolbar.concept.BrowserToolbarTestTags.ADDRESSBAR_URL_BOX import mozilla.components.compose.browser.toolbar.concept.PageOrigin import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent import mozilla.components.compose.browser.toolbar.store.ProgressBarConfig import mozilla.components.compose.browser.toolbar.store.ToolbarGravity import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Bottom -import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Top -import mozilla.components.compose.browser.toolbar.ui.Origin -import mozilla.components.ui.icons.R as iconsR +import mozilla.components.compose.browser.toolbar.ui.FullDisplayToolbar +import mozilla.components.compose.browser.toolbar.ui.MinimalDisplayToolbar +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarDataProvider +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarPreviewModel +import mozilla.components.support.utils.KeyboardState +import mozilla.components.support.utils.keyboardAsState -private const val NO_TOOLBAR_PADDING_DP = 0 -private const val TOOLBAR_PADDING_DP = 8 -private const val LARGE_TOOLBAR_PADDING_DP = 24 +// The value I've observed in my tests. Can differ based on device or keyboard used. +private const val DEFAULT_KEYBOARD_ANIMATION_TIME_MILLIS = 285 /** * Sub-component of the [BrowserToolbar] responsible for displaying the URL and related @@ -78,8 +68,9 @@ private const val LARGE_TOOLBAR_PADDING_DP = 24 * See [MDN docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction). * @param onInteraction Callback for handling [BrowserToolbarEvent]s on user interactions. */ +@OptIn(ExperimentalSharedTransitionApi::class) @Composable -@Suppress("LongMethod", "CyclomaticComplexMethod", "CognitiveComplexMethod") +@Suppress("LongMethod") fun BrowserDisplayToolbar( pageOrigin: PageOrigin, gravity: ToolbarGravity, @@ -90,128 +81,94 @@ fun BrowserDisplayToolbar( browserActionsEnd: List<Action> = emptyList(), onInteraction: (BrowserToolbarEvent) -> Unit, ) { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - val isSmallWidthScreen = remember(windowSizeClass) { - windowSizeClass.minWidthDp < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + val isKeyboardShowing by keyboardAsState() + + val toolbarsTransitionAnimationSpec = remember { + tween<Float>( + durationMillis = DEFAULT_KEYBOARD_ANIMATION_TIME_MILLIS, + easing = LinearEasing, + ) + } + val toolbarElementsAnimationSpec = remember { + tween<Rect>( + durationMillis = DEFAULT_KEYBOARD_ANIMATION_TIME_MILLIS, + easing = FastOutSlowInEasing, + ) } - Surface { - Box( - modifier = Modifier + SharedTransitionLayout { + AnimatedContent( + targetState = gravity == Bottom && isKeyboardShowing == KeyboardState.Opened, + transitionSpec = { + fadeIn(animationSpec = toolbarsTransitionAnimationSpec) togetherWith + fadeOut(animationSpec = toolbarsTransitionAnimationSpec) + }, + ) { isKeyboardShown -> + val toolbarModifier = Modifier .fillMaxWidth() - .padding( - horizontal = when (isSmallWidthScreen) { - true -> NO_TOOLBAR_PADDING_DP.dp - else -> LARGE_TOOLBAR_PADDING_DP.dp - }, + .sharedBounds( + rememberSharedContentState(key = "toolbar_bounds"), + animatedVisibilityScope = this, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(), ) - .semantics { testTagsAsResourceId = true }, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (browserActionsStart.isNotEmpty()) { - ActionContainer( - actions = browserActionsStart, - onInteraction = onInteraction, - ) - } - Row( - modifier = Modifier - .padding( - start = when (browserActionsStart.isEmpty()) { - true -> TOOLBAR_PADDING_DP.dp - false -> NO_TOOLBAR_PADDING_DP.dp - }, - top = TOOLBAR_PADDING_DP.dp, - end = when (browserActionsEnd.isEmpty()) { - true -> TOOLBAR_PADDING_DP.dp - false -> NO_TOOLBAR_PADDING_DP.dp - }, - bottom = when (gravity) { - Top -> TOOLBAR_PADDING_DP - Bottom -> if (browserActionsEnd.isEmpty()) NO_TOOLBAR_PADDING_DP else TOOLBAR_PADDING_DP - }.dp, - ) - .height(48.dp) - .background( - color = MaterialTheme.colorScheme.surfaceDim, - shape = RoundedCornerShape(90.dp), - ) - .padding( - start = when (pageActionsStart.isEmpty()) { - true -> TOOLBAR_PADDING_DP.dp - false -> NO_TOOLBAR_PADDING_DP.dp - }, - top = NO_TOOLBAR_PADDING_DP.dp, - end = when (pageActionsEnd.isEmpty()) { - true -> TOOLBAR_PADDING_DP.dp - false -> NO_TOOLBAR_PADDING_DP.dp - }, - bottom = NO_TOOLBAR_PADDING_DP.dp, - ) - .weight(1f), - verticalAlignment = Alignment.CenterVertically, - ) { - if (pageActionsStart.isNotEmpty()) { - ActionContainer( - actions = pageActionsStart, - onInteraction = onInteraction, - ) - } + val browserActionsStartTrait = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = "browser_actions_start"), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = { _, _ -> toolbarElementsAnimationSpec }, + ) - Origin( - hint = pageOrigin.hint, - modifier = Modifier - .height(56.dp) - .weight(1f) - .testTag(ADDRESSBAR_URL_BOX), - url = pageOrigin.url, - title = pageOrigin.title, - textGravity = pageOrigin.textGravity, - contextualMenuOptions = pageOrigin.contextualMenuOptions, - onClick = pageOrigin.onClick, - onLongClick = pageOrigin.onLongClick, - onInteraction = onInteraction, - ) + val pageActionsStartTrait = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = "page_actions_start"), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = { _, _ -> toolbarElementsAnimationSpec }, + ) - if (pageActionsEnd.isNotEmpty()) { - ActionContainer( - actions = pageActionsEnd, - onInteraction = onInteraction, - ) - } - } + val originTrait = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = "url_box"), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = { _, _ -> toolbarElementsAnimationSpec }, + ) - if (browserActionsEnd.isNotEmpty()) { - ActionContainer( - actions = browserActionsEnd, - onInteraction = onInteraction, - ) - } - } + val pageActionsEndTrait = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = "page_actions_end"), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = { _, _ -> toolbarElementsAnimationSpec }, + ) - HorizontalDivider( - modifier = Modifier.align( - when (gravity) { - Top -> Alignment.BottomCenter - Bottom -> Alignment.TopCenter - }, - ), + val browserActionsEndTrait = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = "browser_actions_end"), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = { _, _ -> toolbarElementsAnimationSpec }, ) - if (progressBarConfig != null) { - AnimatedProgressBar( - progress = progressBarConfig.progress, - color = progressBarConfig.color, - trackColor = Color.Transparent, - modifier = Modifier.align( - when (gravity) { - Top -> Alignment.BottomCenter - Bottom -> Alignment.TopCenter - }, - ), + if (isKeyboardShown) { + MinimalDisplayToolbar( + pageOrigin = pageOrigin, + pageActionsStart = pageActionsStart, + gravity = gravity, + modifier = toolbarModifier, + pageActionsStartModifier = pageActionsStartTrait, + originModifier = originTrait, + ) + } else { + FullDisplayToolbar( + pageOrigin = pageOrigin, + gravity = gravity, + progressBarConfig = progressBarConfig, + browserActionsStart = browserActionsStart, + pageActionsStart = pageActionsStart, + pageActionsEnd = pageActionsEnd, + browserActionsEnd = browserActionsEnd, + onInteraction = onInteraction, + modifier = toolbarModifier, + browserActionsStartModifier = browserActionsStartTrait, + pageActionsStartModifier = pageActionsStartTrait, + originModifier = originTrait, + pageActionsEndModifier = pageActionsEndTrait, + browserActionsEndModifier = browserActionsEndTrait, ) } } @@ -268,85 +225,3 @@ private fun BrowserDisplayToolbarPrivatePreview( ) } } - -private data class DisplayToolbarPreviewModel( - val browserStartActions: List<Action>, - val pageActionsStart: List<Action>, - val title: String?, - val url: String?, - val gravity: ToolbarGravity, - val pageActionsEnd: List<Action>, - val browserEndActions: List<Action>, -) -private class DisplayToolbarDataProvider : PreviewParameterProvider<DisplayToolbarPreviewModel> { - val browserStartActions = listOf( - ActionButtonRes( - drawableResId = iconsR.drawable.mozac_ic_home_24, - contentDescription = android.R.string.untitled, - onClick = object : BrowserToolbarEvent {}, - ), - ) - val pageActionsStart = listOf( - SearchSelectorAction( - icon = DrawableResIcon(iconsR.drawable.mozac_ic_search_24), - contentDescription = StringResContentDescription(resourceId = android.R.string.untitled), - menu = { emptyList() }, - onClick = null, - ), - ) - val pageActionsEnd = listOf( - ActionButtonRes( - drawableResId = iconsR.drawable.mozac_ic_arrow_clockwise_24, - contentDescription = android.R.string.untitled, - onClick = object : BrowserToolbarEvent {}, - ), - ) - val browserActionsEnd = listOf( - ActionButtonRes( - drawableResId = iconsR.drawable.mozac_ic_ellipsis_vertical_24, - contentDescription = android.R.string.untitled, - onClick = object : BrowserToolbarEvent {}, - ), - ) - val title = "Firefox" - val url = "mozilla.com/firefox" - - override val values = sequenceOf( - DisplayToolbarPreviewModel( - browserStartActions = browserStartActions, - pageActionsStart = pageActionsStart, - title = title, - url = url, - gravity = Top, - pageActionsEnd = pageActionsEnd, - browserEndActions = browserActionsEnd, - ), - DisplayToolbarPreviewModel( - browserStartActions = emptyList(), - pageActionsStart = pageActionsStart, - title = null, - url = url, - gravity = Bottom, - pageActionsEnd = pageActionsEnd, - browserEndActions = emptyList(), - ), - DisplayToolbarPreviewModel( - browserStartActions = browserStartActions, - pageActionsStart = emptyList(), - title = title, - url = url, - gravity = Top, - pageActionsEnd = emptyList(), - browserEndActions = browserActionsEnd, - ), - DisplayToolbarPreviewModel( - browserStartActions = emptyList(), - pageActionsStart = emptyList(), - title = null, - url = null, - gravity = Bottom, - pageActionsEnd = emptyList(), - browserEndActions = emptyList(), - ), - ) -} diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/concept/BrowserToolbarTestTags.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/concept/BrowserToolbarTestTags.kt @@ -10,6 +10,8 @@ import mozilla.components.compose.browser.toolbar.BrowserToolbar * Test tags for the [BrowserToolbar] composable. */ object BrowserToolbarTestTags { + const val MINIMAL_ADDRESS_BAR = "MINIMAL_ADDRESS_BAR" + /** * Test tag for the website origin box while in "display" mode. */ diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/FullDisplayToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/FullDisplayToolbar.kt @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.compose.browser.toolbar.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass +import mozilla.components.compose.base.progressbar.AnimatedProgressBar +import mozilla.components.compose.base.theme.AcornTheme +import mozilla.components.compose.base.theme.acornPrivateColorScheme +import mozilla.components.compose.base.theme.privateColorPalette +import mozilla.components.compose.browser.toolbar.ActionContainer +import mozilla.components.compose.browser.toolbar.R +import mozilla.components.compose.browser.toolbar.concept.Action +import mozilla.components.compose.browser.toolbar.concept.BrowserToolbarTestTags.ADDRESSBAR_URL_BOX +import mozilla.components.compose.browser.toolbar.concept.PageOrigin +import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent +import mozilla.components.compose.browser.toolbar.store.ProgressBarConfig +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Bottom +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Top +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarDataProvider +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarPreviewModel + +private const val NO_TOOLBAR_PADDING_DP = 0 +private const val TOOLBAR_PADDING_DP = 8 +private const val LARGE_TOOLBAR_PADDING_DP = 24 + +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod", "CognitiveComplexMethod") +@Composable +internal fun FullDisplayToolbar( + pageOrigin: PageOrigin, + gravity: ToolbarGravity, + progressBarConfig: ProgressBarConfig?, + browserActionsStart: List<Action>, + pageActionsStart: List<Action>, + pageActionsEnd: List<Action>, + browserActionsEnd: List<Action>, + onInteraction: (BrowserToolbarEvent) -> Unit, + modifier: Modifier = Modifier, + browserActionsStartModifier: Modifier = Modifier, + pageActionsStartModifier: Modifier = Modifier, + originModifier: Modifier = Modifier, + pageActionsEndModifier: Modifier = Modifier, + browserActionsEndModifier: Modifier = Modifier, +) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isSmallWidthScreen = remember(windowSizeClass) { + windowSizeClass.minWidthDp < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + } + + Surface { + Box( + modifier = modifier + .padding( + horizontal = when (isSmallWidthScreen) { + true -> NO_TOOLBAR_PADDING_DP.dp + else -> LARGE_TOOLBAR_PADDING_DP.dp + }, + ) + .semantics { testTagsAsResourceId = true }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (browserActionsStart.isNotEmpty()) { + ActionContainer( + actions = browserActionsStart, + onInteraction = onInteraction, + modifier = browserActionsStartModifier, + ) + } + + Row( + modifier = Modifier + .padding( + start = when (browserActionsStart.isEmpty()) { + true -> TOOLBAR_PADDING_DP.dp + false -> NO_TOOLBAR_PADDING_DP.dp + }, + top = TOOLBAR_PADDING_DP.dp, + end = when (browserActionsEnd.isEmpty()) { + true -> TOOLBAR_PADDING_DP.dp + false -> NO_TOOLBAR_PADDING_DP.dp + }, + bottom = when (gravity) { + Top -> TOOLBAR_PADDING_DP + Bottom -> if (browserActionsEnd.isEmpty()) NO_TOOLBAR_PADDING_DP else TOOLBAR_PADDING_DP + }.dp, + ) + .height(48.dp) + .background( + color = MaterialTheme.colorScheme.surfaceDim, + shape = RoundedCornerShape(90.dp), + ) + .padding( + start = when (pageActionsStart.isEmpty()) { + true -> TOOLBAR_PADDING_DP.dp + false -> NO_TOOLBAR_PADDING_DP.dp + }, + top = NO_TOOLBAR_PADDING_DP.dp, + end = when (pageActionsEnd.isEmpty()) { + true -> TOOLBAR_PADDING_DP.dp + false -> NO_TOOLBAR_PADDING_DP.dp + }, + bottom = NO_TOOLBAR_PADDING_DP.dp, + ) + .weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + if (pageActionsStart.isNotEmpty()) { + ActionContainer( + actions = pageActionsStart, + onInteraction = onInteraction, + modifier = pageActionsStartModifier, + ) + } + + Origin( + hint = pageOrigin.hint, + modifier = Modifier + .height(56.dp) + .weight(1f) + .testTag(ADDRESSBAR_URL_BOX) + .then(originModifier), + url = pageOrigin.url, + title = pageOrigin.title, + textGravity = pageOrigin.textGravity, + contextualMenuOptions = pageOrigin.contextualMenuOptions, + onClick = pageOrigin.onClick, + onLongClick = pageOrigin.onLongClick, + onInteraction = onInteraction, + ) + + if (pageActionsEnd.isNotEmpty()) { + ActionContainer( + actions = pageActionsEnd, + onInteraction = onInteraction, + modifier = pageActionsEndModifier, + ) + } + } + + if (browserActionsEnd.isNotEmpty()) { + ActionContainer( + actions = browserActionsEnd, + onInteraction = onInteraction, + modifier = browserActionsEndModifier, + ) + } + } + + HorizontalDivider( + modifier = Modifier.align( + when (gravity) { + Top -> Alignment.BottomCenter + Bottom -> Alignment.TopCenter + }, + ), + ) + + if (progressBarConfig != null) { + AnimatedProgressBar( + progress = progressBarConfig.progress, + color = progressBarConfig.color, + trackColor = Color.Transparent, + modifier = Modifier.align( + when (gravity) { + Top -> Alignment.BottomCenter + Bottom -> Alignment.TopCenter + }, + ), + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun FullDisplayToolbarPreview( + @PreviewParameter(DisplayToolbarDataProvider::class) config: DisplayToolbarPreviewModel, +) { + AcornTheme { + FullDisplayToolbar( + gravity = config.gravity, + progressBarConfig = ProgressBarConfig(progress = 66), + browserActionsStart = config.browserStartActions, + pageActionsStart = config.pageActionsStart, + pageOrigin = PageOrigin( + hint = R.string.mozac_browser_toolbar_search_hint, + title = config.title, + url = config.url, + onClick = object : BrowserToolbarEvent {}, + ), + pageActionsEnd = config.pageActionsEnd, + browserActionsEnd = config.browserEndActions, + onInteraction = {}, + ) + } +} + +@Preview +@Composable +private fun FullDisplayToolbarPrivatePreview( + @PreviewParameter(DisplayToolbarDataProvider::class) config: DisplayToolbarPreviewModel, +) { + AcornTheme( + colors = privateColorPalette, + colorScheme = acornPrivateColorScheme(), + ) { + FullDisplayToolbar( + gravity = config.gravity, + progressBarConfig = ProgressBarConfig(progress = 66), + browserActionsStart = config.browserStartActions, + pageActionsStart = config.pageActionsStart, + pageOrigin = PageOrigin( + hint = R.string.mozac_browser_toolbar_search_hint, + title = config.title, + url = config.url, + onClick = object : BrowserToolbarEvent {}, + ), + pageActionsEnd = config.pageActionsEnd, + browserActionsEnd = config.browserEndActions, + onInteraction = {}, + ) + } +} diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/MinimalDisplayToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/MinimalDisplayToolbar.kt @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.compose.browser.toolbar.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.theme.AcornTheme +import mozilla.components.compose.browser.toolbar.ActionContainer +import mozilla.components.compose.browser.toolbar.R +import mozilla.components.compose.browser.toolbar.concept.Action +import mozilla.components.compose.browser.toolbar.concept.BrowserToolbarTestTags.ADDRESSBAR_URL_BOX +import mozilla.components.compose.browser.toolbar.concept.BrowserToolbarTestTags.MINIMAL_ADDRESS_BAR +import mozilla.components.compose.browser.toolbar.concept.PageOrigin +import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Bottom +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Top +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarDataProvider +import mozilla.components.compose.browser.toolbar.utils.DisplayToolbarPreviewModel +import mozilla.components.support.ktx.kotlin.getRegistrableDomainIndexRange + +private const val MINIMAL_TOOLBAR_HEIGHT_DP = 32 + +@Composable +internal fun MinimalDisplayToolbar( + pageOrigin: PageOrigin, + pageActionsStart: List<Action>, + gravity: ToolbarGravity, + modifier: Modifier = Modifier, + pageActionsStartModifier: Modifier = Modifier, + originModifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val registrableDomain = remember(pageOrigin.url) { + val url = pageOrigin.url ?: return@remember "" + val (start, end) = url.getRegistrableDomainIndexRange() ?: return@remember url + url.substring(start, end) + } + val contentDescription = stringResource( + R.string.mozac_minimal_display_toolbar_content_description, + registrableDomain, + ) + + Surface { + Box { + Row( + modifier = modifier + .requiredHeight(MINIMAL_TOOLBAR_HEIGHT_DP.dp) + .clearAndSetSemantics { + this.contentDescription = contentDescription + testTagsAsResourceId = true + testTag = MINIMAL_ADDRESS_BAR + } + .pointerInput(Unit) { + focusRequester.requestFocus() + keyboardController?.hide() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + if (pageActionsStart.isNotEmpty()) { + ActionContainer( + actions = pageActionsStart, + onInteraction = {}, + modifier = pageActionsStartModifier, + ) + } + + Origin( + hint = pageOrigin.hint, + url = registrableDomain, + title = null, + textGravity = pageOrigin.textGravity, + contextualMenuOptions = pageOrigin.contextualMenuOptions, + onClick = null, + onLongClick = null, + onInteraction = {}, + modifier = Modifier + .testTag(ADDRESSBAR_URL_BOX) + .then(originModifier), + ) + } + + HorizontalDivider( + modifier = Modifier.align( + when (gravity) { + Top -> Alignment.BottomCenter + Bottom -> Alignment.TopCenter + }, + ), + ) + } + } +} + +@PreviewLightDark +@Composable +private fun MinimalDisplayToolbarPreview( + @PreviewParameter(DisplayToolbarDataProvider::class) config: DisplayToolbarPreviewModel, +) { + AcornTheme { + Surface { + MinimalDisplayToolbar( + pageOrigin = PageOrigin( + hint = R.string.mozac_browser_toolbar_search_hint, + title = config.title, + url = config.url, + onClick = object : BrowserToolbarEvent {}, + ), + pageActionsStart = config.pageActionsStart, + gravity = config.gravity, + ) + } + } +} diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/utils/DisplayToolbarDataProvider.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/utils/DisplayToolbarDataProvider.kt @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.compose.browser.toolbar.utils + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import mozilla.components.compose.browser.toolbar.concept.Action +import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes +import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction +import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction.ContentDescription.StringResContentDescription +import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction.Icon.DrawableResIcon +import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Bottom +import mozilla.components.compose.browser.toolbar.store.ToolbarGravity.Top +import mozilla.components.ui.icons.R as iconsR + +internal class DisplayToolbarDataProvider : PreviewParameterProvider<DisplayToolbarPreviewModel> { + val browserStartActions = listOf( + ActionButtonRes( + drawableResId = iconsR.drawable.mozac_ic_home_24, + contentDescription = android.R.string.untitled, + onClick = object : BrowserToolbarEvent {}, + ), + ) + val pageActionsStart = listOf( + SearchSelectorAction( + icon = DrawableResIcon(iconsR.drawable.mozac_ic_search_24), + contentDescription = StringResContentDescription(resourceId = android.R.string.untitled), + menu = { emptyList() }, + onClick = null, + ), + ) + val pageActionsEnd = listOf( + ActionButtonRes( + drawableResId = iconsR.drawable.mozac_ic_arrow_clockwise_24, + contentDescription = android.R.string.untitled, + onClick = object : BrowserToolbarEvent {}, + ), + ) + val browserActionsEnd = listOf( + ActionButtonRes( + drawableResId = iconsR.drawable.mozac_ic_ellipsis_vertical_24, + contentDescription = android.R.string.untitled, + onClick = object : BrowserToolbarEvent {}, + ), + ) + val title = "Firefox" + val url = "mozilla.com/firefox" + + override val values = sequenceOf( + DisplayToolbarPreviewModel( + browserStartActions = browserStartActions, + pageActionsStart = pageActionsStart, + title = title, + url = url, + gravity = Top, + pageActionsEnd = pageActionsEnd, + browserEndActions = browserActionsEnd, + ), + DisplayToolbarPreviewModel( + browserStartActions = emptyList(), + pageActionsStart = pageActionsStart, + title = null, + url = url, + gravity = Bottom, + pageActionsEnd = pageActionsEnd, + browserEndActions = emptyList(), + ), + DisplayToolbarPreviewModel( + browserStartActions = browserStartActions, + pageActionsStart = emptyList(), + title = title, + url = url, + gravity = Top, + pageActionsEnd = emptyList(), + browserEndActions = browserActionsEnd, + ), + DisplayToolbarPreviewModel( + browserStartActions = emptyList(), + pageActionsStart = emptyList(), + title = null, + url = null, + gravity = Bottom, + pageActionsEnd = emptyList(), + browserEndActions = emptyList(), + ), + ) +} + +internal data class DisplayToolbarPreviewModel( + val browserStartActions: List<Action>, + val pageActionsStart: List<Action>, + val title: String?, + val url: String?, + val gravity: ToolbarGravity, + val pageActionsEnd: List<Action>, + val browserEndActions: List<Action>, +) diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/browser-toolbar/src/main/res/values/strings.xml @@ -17,4 +17,6 @@ <string name="mozac_toolbar_cfr_title">Designed for Android. Refined for You</string> <!-- Text for the description displayed in the contextual feature recommendation popup promoting the new toolbar redesign. --> <string name="mozac_toolbar_cfr_description">We’ve refreshed the design with a sleek, modern feel</string> + <!-- Content description (not visible, for screen readers etc.) for the "minimal toolbar" containing just the URL instead of the big toolbar containing also other buttons. %1$s will be replaced with the domain name of the current website. --> + <string name="mozac_minimal_display_toolbar_content_description">Entering text in %1$s. Double tap to stop</string> </resources> diff --git a/mobile/android/android-components/config/detekt-baseline.xml b/mobile/android/android-components/config/detekt-baseline.xml @@ -18,7 +18,7 @@ <ID>ForbiddenSuppress:AppLinksInterceptor.kt$AppLinksInterceptor$@Suppress("CognitiveComplexMethod", "ReturnCount", "CyclomaticComplexMethod")</ID> <ID>ForbiddenSuppress:AppLinksUseCases.kt$AppLinksUseCases.GetAppLinkRedirect$@Suppress("CyclomaticComplexMethod")</ID> <ID>ForbiddenSuppress:BaseBrowserFragment.kt$BaseBrowserFragment$@Suppress("LongMethod")</ID> - <ID>ForbiddenSuppress:BrowserDisplayToolbar.kt$@Suppress("LongMethod", "CyclomaticComplexMethod", "CognitiveComplexMethod")</ID> + <ID>ForbiddenSuppress:BrowserDisplayToolbar.kt$@Suppress("LongMethod")</ID> <ID>ForbiddenSuppress:BrowserFragment.kt$BrowserFragment$@Suppress("LongMethod")</ID> <ID>ForbiddenSuppress:BrowserMenuPositioning.kt$@Suppress("CyclomaticComplexMethod")</ID> <ID>ForbiddenSuppress:BrowserScreen.kt$@Suppress("LongMethod")</ID> @@ -48,6 +48,7 @@ <ID>ForbiddenSuppress:EngineStateReducer.kt$EngineStateReducer$@Suppress("LongMethod")</ID> <ID>ForbiddenSuppress:EngineVersion.kt$EngineVersion.Companion$@Suppress("MagicNumber", "ReturnCount")</ID> <ID>ForbiddenSuppress:ExpandableLayout.kt$ExpandableLayout$@Suppress("ReturnCount", "CognitiveComplexMethod")</ID> + <ID>ForbiddenSuppress:FullDisplayToolbar.kt$@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod", "CognitiveComplexMethod")</ID> <ID>ForbiddenSuppress:FxaDeviceConstellation.kt$FxaDeviceConstellation$@Suppress("CognitiveComplexMethod")</ID> <ID>ForbiddenSuppress:GeckoEngineSession.kt$GeckoEngineSession$@Suppress("CognitiveComplexMethod")</ID> <ID>ForbiddenSuppress:GeckoEngineSession.kt$GeckoEngineSession$@Suppress("NestedBlockDepth", "CognitiveComplexMethod")</ID>