Skip to content

Commit

Permalink
PM-16631 Applying CoachMarkContainer to the AddLoginItem content. (#4571
Browse files Browse the repository at this point in the history
)
  • Loading branch information
dseverns-livefront authored Jan 21, 2025
1 parent 08e51fd commit 2b94e01
Show file tree
Hide file tree
Showing 19 changed files with 1,752 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
Expand Down Expand Up @@ -156,13 +166,55 @@ fun Modifier.tabNavigation(): Modifier {
*/
@OmitFromCoverage
@Stable
@Composable
fun Modifier.standardHorizontalMargin(
portrait: Dp = 16.dp,
landscape: Dp = 48.dp,
): Modifier {
val config = LocalConfiguration.current
return this.padding(horizontal = if (config.isPortrait) portrait else landscape)
): Modifier =
this then StandardHorizontalMarginElement(portrait = portrait, landscape = landscape)

private data class StandardHorizontalMarginElement(
private val portrait: Dp,
private val landscape: Dp,
) : ModifierNodeElement<StandardHorizontalMarginElement.StandardHorizontalMarginConsumerNode>() {
override fun create(): StandardHorizontalMarginConsumerNode =
StandardHorizontalMarginConsumerNode(
portrait = portrait,
landscape = landscape,
)

override fun update(node: StandardHorizontalMarginConsumerNode) {
node.portrait = portrait
node.landscape = landscape
}

class StandardHorizontalMarginConsumerNode(
var portrait: Dp,
var landscape: Dp,
) : Modifier.Node(),
LayoutModifierNode,
CompositionLocalConsumerModifierNode {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val currentConfig = currentValueOf(LocalConfiguration)
val paddingPx = (if (currentConfig.isPortrait) portrait else landscape).roundToPx()
// Account for the padding on each side.
val horizontalPx = paddingPx * 2
// Measure the placeable within the horizontal space accounting for the padding Px.
val placeable = measurable.measure(
constraints = constraints.offset(
horizontal = -horizontalPx,
vertical = 0,
),
)
// The width of the placeable plus the total padding, used to create the layout.
val width = constraints.constrainWidth(width = placeable.width + horizontalPx)
return layout(width = width, height = placeable.height) {
placeable.place(x = paddingPx, y = 0)
}
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.x8bit.bitwarden.ui.platform.components.coachmark

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme

/**
* Clickable text used for the standard action UI for a Coach Mark which applies
* correct text style by default.
*/
@Composable
fun CoachMarkActionText(
actionLabel: String,
onActionClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BitwardenClickableText(
label = actionLabel,
onClick = onActionClick,
style = BitwardenTheme.typography.labelLarge,
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package com.x8bit.bitwarden.ui.platform.components.coachmark

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.launch

private const val ROUNDED_RECT_RADIUS = 8f

/**
* A composable container that manages and displays coach mark highlights.
*
* This composable provides a full-screen overlay that can highlight specific
* areas of the UI and display tooltips to guide the user through a sequence
* of steps or features.
*
* @param T The type of the enum used to represent the unique keys for each coach mark highlight.
* @param state The [CoachMarkState] that manages the sequence and state of the coach marks.
* @param modifier The modifier to be applied to the container.
* @param content The composable content that defines the coach mark highlights within the
* [CoachMarkScope].
*/
@Composable
@Suppress("LongMethod")
fun <T : Enum<T>> CoachMarkContainer(
state: CoachMarkState<T>,
modifier: Modifier = Modifier,
content: @Composable CoachMarkScope<T>.() -> Unit,
) {
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
) {
CoachMarkScopeInstance(coachMarkState = state).content()
val boundedRectangle by state.currentHighlightBounds
val isVisible by state.isVisible
val currentHighlightShape by state.currentHighlightShape

val highlightPath = remember(boundedRectangle, currentHighlightShape) {
if (boundedRectangle == Rect.Zero) {
return@remember Path()
}
val highlightArea = Rect(
topLeft = boundedRectangle.topLeft,
bottomRight = boundedRectangle.bottomRight,
)
Path().apply {
when (currentHighlightShape) {
CoachMarkHighlightShape.SQUARE -> addRoundRect(
RoundRect(
rect = highlightArea,
cornerRadius = CornerRadius(
x = ROUNDED_RECT_RADIUS,
),
),
)

CoachMarkHighlightShape.OVAL -> addOval(highlightArea)
}
}
}
if (boundedRectangle != Rect.Zero && isVisible) {
val backgroundColor = BitwardenTheme.colorScheme.background.scrim
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
scope.launch {
state.showToolTipForCurrentCoachMark()
}
},
)
}
.fillMaxSize()
.drawBehind {
clipPath(
path = highlightPath,
clipOp = ClipOp.Difference,
block = {
drawRect(
color = backgroundColor,
)
},
)
},
)
}
// Once the bounds and shape update show the tooltip for the active coach mark.
LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
if (state.currentHighlightBounds.value != Rect.Zero) {
state.showToolTipForCurrentCoachMark()
}
}
// On the initial composition of the screen check to see if the coach mark was visible and
// then show the associated coach mark.
LaunchedEffect(Unit) {
if (state.isVisible.value) {
state.currentHighlight.value?.let {
state.showCoachMark(it)
}
}
}
}
}

@Preview
@Composable
@Suppress("LongMethod")
private fun BitwardenCoachMarkContainer_preview() {
BitwardenTheme {
val state = rememberCoachMarkState(Foo.entries)
val scope = rememberCoroutineScope()
CoachMarkContainer(
state = state,
) {
Column(
modifier = Modifier
.background(BitwardenTheme.colorScheme.background.primary)
.padding(top = 100.dp)
.padding(horizontal = 16.dp)
.fillMaxSize(),
) {

BitwardenClickableText(
label = "Start Coach Mark Flow",
onClick = {
scope.launch {
state.showCoachMark(Foo.Bar)
}
},
style = BitwardenTheme.typography.labelLarge,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(Modifier.height(24.dp))
Row(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.weight(1f))
CoachMarkHighlight(
key = Foo.Bar,
title = "1 of 3",
description = "Use this button to generate a new unique password.",
rightAction = {
BitwardenClickableText(
label = "Next",
onClick = {
scope.launch {
state.showNextCoachMark()
}
},
style = BitwardenTheme.typography.labelLarge,
)
},
shape = CoachMarkHighlightShape.OVAL,
) {
BitwardenStandardIconButton(
painter = rememberVectorPainter(R.drawable.ic_puzzle),
contentDescription = stringResource(R.string.close),
onClick = {},
)
}
}
Spacer(Modifier.height(24.dp))
CoachMarkHighlight(
key = Foo.Baz,
title = "Foo",
description = "Baz",
leftAction = {
BitwardenClickableText(
label = "Back",
onClick = {
scope.launch {
state.showPreviousCoachMark()
}
},
style = BitwardenTheme.typography.labelLarge,
)
},
rightAction = {
BitwardenClickableText(
label = "Done",
onClick = {
scope.launch {
state.coachingComplete()
}
},
style = BitwardenTheme.typography.labelLarge,
)
},
) {
Text(text = "Foo Baz")
}

Spacer(Modifier.size(100.dp))
}
}
}
}

/**
* Example enum for demonstration purposes.
*/
private enum class Foo {
Bar,
Baz,
}
Loading

0 comments on commit 2b94e01

Please sign in to comment.