Skip to content

Commit

Permalink
Merge pull request #458 from kiwicom/stepper-semantics
Browse files Browse the repository at this point in the history
Implement top-node semantics for Stepper (BC break!)
  • Loading branch information
hrach committed Jun 12, 2023
2 parents a66f74c + 44c8196 commit 707895d
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 3 deletions.
68 changes: 68 additions & 0 deletions docs/03-components/07-interaction/stepper.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Android
---

## Overview

`Stepper` component is provided as a single value component in two variants:

- [`Stepper` with min & max values](https://kiwicom.github.io/orbit-compose/ui/kiwi.orbit.compose.ui.controls/-stepper.html)
- [`Stepper` with a custom validator](https://kiwicom.github.io/orbit-compose/ui/kiwi.orbit.compose.ui.controls/-stepper.html)

## Usage

Use the `Stepper` composable and define its value and value change callback. Optionally, set up the value boundaries using `minValue` and `maxValue`. The overloaded implementation allows you to set up a custom validator and also set custom `contentDescription` labels for the increase and decrease actions.

The whole component can be info-color highlighted by setting it as `active`.

```kotlin
@Composable
fun Example() {
var value by remember { mutableIntStateOf(0) }
Stepper(
modifier = Modifier.testTag("stepper"),
value = value,
onValueChange = { value = it },
active = value > 0,
minValue = 0,
maxValue = 10,
)
}
```

## UI Testing

Use semantic properties and custom actions to read the value and increase/decrease it.

If the button is not active, the particular semantic action is not added, use `fetchSemanticsNode()` to check its presence.

```kotlin
composeTestRule.setContent {
var value by remember { mutableIntStateOf(0) }
Stepper(
modifier = Modifier.testTag("stepper"),
value = value,
onValueChange = { value = it },
)
}
val stepper = composeTestRule.onNodeWithTag("stepper")

assertFalse(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.DecreaseValue))
assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.IncreaseValue))

stepper
.assertTextEquals("0")
.performSemanticsAction(StepperSemanticsActions.IncreaseValue)
.assertTextEquals("1")
.performSemanticsAction(StepperSemanticsActions.IncreaseValue)
.assertTextEquals("2")
.performSemanticsAction(StepperSemanticsActions.DecreaseValue)
.assertTextEquals("1")

assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.DecreaseValue))
assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.IncreaseValue))
```

## Customization

`Stepper` is not customizable.
1 change: 1 addition & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
1. [Collapse](03-components/07-interaction/collapse.mdx)
1. [Loading / LinearProgressIndicator](03-components/10-progress-indicators/loading.mdx)
1. [Slider](03-components/07-interaction/slider.mdx)
1. [Stepper](03-components/07-interaction/stepper.mdx)
1. [Tabs](03-components/02-structure/tabs.mdx)
1. [Tile](03-components/02-structure/tile.mdx)
1. [TileGroup](03-components/02-structure/tilegroup.mdx)
1 change: 1 addition & 0 deletions ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ dependencies {
debugImplementation(libs.androidx.customView)
debugImplementation(libs.androidx.customViewPoolingContainer)

testImplementation(kotlin("test"))
testImplementation(libs.robolectric)
testImplementation(libs.compose.uiTest)
testImplementation(libs.compose.uiTestManifest)
Expand Down
30 changes: 27 additions & 3 deletions ui/src/main/java/kiwi/orbit/compose/ui/controls/Stepper.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@file:Suppress("MatchingDeclarationName")
// ktlint-disable filename

package kiwi.orbit.compose.ui.controls

import androidx.compose.animation.AnimatedContent
Expand Down Expand Up @@ -25,7 +28,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kiwi.orbit.compose.icons.Icons
Expand All @@ -39,6 +45,13 @@ import kiwi.orbit.compose.ui.foundation.LocalContentEmphasis
import kiwi.orbit.compose.ui.foundation.LocalTextStyle
import kiwi.orbit.compose.ui.foundation.contentColorFor

public object StepperSemanticsActions {
public val IncreaseValue: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>> =
SemanticsPropertyKey("IncreaseValue")
public val DecreaseValue: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>> =
SemanticsPropertyKey("DecreaseValue")
}

@Composable
public fun Stepper(
value: Int,
Expand Down Expand Up @@ -92,14 +105,25 @@ private fun StepperPrimitive(
valueValidator: ((Int) -> Boolean)?,
modifier: Modifier = Modifier,
) {
val isDecreaseValid = valueValidator?.invoke(value - 1) ?: true
val isIncreaseValid = valueValidator?.invoke(value + 1) ?: true
Row(
modifier = modifier,
modifier = modifier.semantics(mergeDescendants = true) {
if (isDecreaseValid) {
this[StepperSemanticsActions.DecreaseValue] =
AccessibilityAction(null) { onValueChange.invoke(value - 1); true }
}
if (isIncreaseValid) {
this[StepperSemanticsActions.IncreaseValue] =
AccessibilityAction(null) { onValueChange.invoke(value + 1); true }
}
},
verticalAlignment = Alignment.CenterVertically,
) {
StepperButton(
onClick = { onValueChange.invoke(value - 1) },
active = active,
enabled = valueValidator?.invoke(value - 1) ?: true,
enabled = isDecreaseValid,
) {
Icon(Icons.Minus, contentDescription = removeContentDescription)
}
Expand Down Expand Up @@ -133,7 +157,7 @@ private fun StepperPrimitive(
StepperButton(
onClick = { onValueChange.invoke(value + 1) },
active = active,
enabled = valueValidator?.invoke(value + 1) ?: true,
enabled = isIncreaseValid,
) {
Icon(Icons.Plus, contentDescription = addContentDescription)
}
Expand Down
52 changes: 52 additions & 0 deletions ui/src/test/kotlin/kiwi/orbit/compose/ui/controls/StepperTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package kiwi.orbit.compose.ui.controls

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performSemanticsAction
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class StepperTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testBasics() {
composeTestRule.setContent {
var value by remember { mutableIntStateOf(0) }
Stepper(
modifier = Modifier.testTag("stepper"),
value = value,
onValueChange = { value = it },
)
}
val stepper = composeTestRule.onNodeWithTag("stepper")

assertFalse(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.DecreaseValue))
assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.IncreaseValue))

stepper
.assertTextEquals("0")
.performSemanticsAction(StepperSemanticsActions.IncreaseValue)
.assertTextEquals("1")
.performSemanticsAction(StepperSemanticsActions.IncreaseValue)
.assertTextEquals("2")
.performSemanticsAction(StepperSemanticsActions.DecreaseValue)
.assertTextEquals("1")

assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.DecreaseValue))
assertTrue(stepper.fetchSemanticsNode().config.contains(StepperSemanticsActions.IncreaseValue))
}
}

0 comments on commit 707895d

Please sign in to comment.