fix: correct ABox switch semantics and motion
This commit is contained in:
+63
-19
@@ -1,35 +1,38 @@
|
||||
package com.flagship.abox.manager.designsystem
|
||||
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.assertContentDescriptionEquals
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertHeightIsAtLeast
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertWidthIsAtLeast
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
|
||||
import com.flagship.abox.manager.preferences.AppThemeMode
|
||||
import com.flagship.abox.manager.preferences.DesignRenderer
|
||||
import com.flagship.abox.manager.preferences.UiPreferences
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.material3.Text
|
||||
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ABoxComponentSemanticsTest {
|
||||
@get:Rule
|
||||
@@ -39,27 +42,58 @@ class ABoxComponentSemanticsTest {
|
||||
fun allInteractiveControlsHaveAtLeast48DpBounds() {
|
||||
composeRule.setContent {
|
||||
ABoxTheme {
|
||||
ABoxButton(onClick = {}, modifier = androidx.compose.ui.Modifier.testTag("button")) {
|
||||
androidx.compose.material3.Text("确定")
|
||||
ABoxButton(onClick = {}, modifier = Modifier.testTag("button")) {
|
||||
Text("确定")
|
||||
}
|
||||
ABoxIconButton(
|
||||
key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.ADD,
|
||||
key = ABoxIconKey.ADD,
|
||||
contentDescription = "添加设备",
|
||||
onClick = {},
|
||||
modifier = androidx.compose.ui.Modifier.testTag("icon"),
|
||||
modifier = Modifier.testTag("icon"),
|
||||
)
|
||||
ABoxSwitch(
|
||||
checked = false,
|
||||
onCheckedChange = {},
|
||||
contentDescription = "启用设备",
|
||||
modifier = androidx.compose.ui.Modifier.testTag("switch"),
|
||||
modifier = Modifier.testTag("switch"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNodeWithTag("button").assertHeightIsAtLeast(48.dp)
|
||||
composeRule.onNodeWithTag("icon").assertHeightIsAtLeast(48.dp)
|
||||
composeRule.onNodeWithTag("switch").assertHeightIsAtLeast(48.dp)
|
||||
listOf("button", "icon", "switch").forEach { tag ->
|
||||
composeRule.onNodeWithTag(tag)
|
||||
.assertWidthIsAtLeast(48.dp)
|
||||
.assertHeightIsAtLeast(48.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun operableSwitchExposesChineseLabelRoleStateAndToggleAction() {
|
||||
var checked = false
|
||||
composeRule.setContent {
|
||||
ABoxTheme {
|
||||
ABoxSwitch(
|
||||
checked = checked,
|
||||
onCheckedChange = { checked = it },
|
||||
contentDescription = "启用设备",
|
||||
modifier = Modifier.testTag("operable-switch"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val switchNode = composeRule.onNodeWithTag("operable-switch")
|
||||
.assertContentDescriptionEquals("启用设备")
|
||||
.assertIsOff()
|
||||
.assertHasClickAction()
|
||||
.assertWidthIsAtLeast(48.dp)
|
||||
.assertHeightIsAtLeast(48.dp)
|
||||
.fetchSemanticsNode()
|
||||
assertEquals(Role.Switch, switchNode.config[SemanticsProperties.Role])
|
||||
assertTrue(switchNode.config.contains(SemanticsProperties.ToggleableState))
|
||||
|
||||
composeRule.onNodeWithTag("operable-switch").performClick()
|
||||
composeRule.waitForIdle()
|
||||
assertEquals(true, checked)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,7 +105,7 @@ class ABoxComponentSemanticsTest {
|
||||
subtitle = "在线",
|
||||
onClick = {},
|
||||
quickAction = ABoxCardQuickAction(
|
||||
key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.REFRESH,
|
||||
key = ABoxIconKey.REFRESH,
|
||||
contentDescription = "刷新设备",
|
||||
onClick = {},
|
||||
),
|
||||
@@ -108,9 +142,19 @@ class ABoxComponentSemanticsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reducedMotionExposesStaticLoadingState() {
|
||||
fun reducedMotionExposesStaticLoadingStateThroughThemeSeam() {
|
||||
composeRule.setContent {
|
||||
CompositionLocalProvider(LocalABoxReducedMotion provides true) {
|
||||
ABoxThemeContent(
|
||||
preferences = UiPreferences(
|
||||
renderer = DesignRenderer.MATERIAL3,
|
||||
themeMode = AppThemeMode.LIGHT,
|
||||
dynamicColorEnabled = false,
|
||||
),
|
||||
systemInDarkTheme = false,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
dynamicColorTokens = null,
|
||||
reducedMotion = true,
|
||||
) {
|
||||
ABoxLoadingState(contentDescription = "正在加载")
|
||||
}
|
||||
}
|
||||
@@ -126,7 +170,7 @@ class ABoxComponentSemanticsTest {
|
||||
composeRule.setContent {
|
||||
ABoxTheme {
|
||||
ABoxIconButton(
|
||||
key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.BACK,
|
||||
key = ABoxIconKey.BACK,
|
||||
contentDescription = "返回",
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
+10
-1
@@ -54,6 +54,8 @@ import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -135,6 +137,12 @@ fun ABoxSwitch(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = enabled && onCheckedChange != null,
|
||||
role = Role.Switch,
|
||||
onValueChange = { onCheckedChange?.invoke(it) },
|
||||
)
|
||||
.semantics {
|
||||
this.contentDescription = contentDescription
|
||||
},
|
||||
@@ -142,8 +150,9 @@ fun ABoxSwitch(
|
||||
) {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+33
-3
@@ -1,6 +1,7 @@
|
||||
package com.flagship.abox.manager.designsystem
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
@@ -35,12 +36,12 @@ fun ABoxTheme(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val systemInDarkTheme = androidx.compose.foundation.isSystemInDarkTheme()
|
||||
val context = LocalContext.current
|
||||
val isDarkTheme = when (preferences.themeMode) {
|
||||
AppThemeMode.SYSTEM -> systemInDarkTheme
|
||||
AppThemeMode.LIGHT -> false
|
||||
AppThemeMode.DARK -> true
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val dynamicTokens = if (preferences.dynamicColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val dynamicColorScheme = if (isDarkTheme) {
|
||||
dynamicDarkColorScheme(context)
|
||||
@@ -51,11 +52,30 @@ fun ABoxTheme(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val resolution = ABoxThemeResolver.resolve(
|
||||
ABoxThemeContent(
|
||||
preferences = preferences,
|
||||
systemInDarkTheme = systemInDarkTheme,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
dynamicColorTokens = dynamicTokens,
|
||||
reducedMotion = systemAnimatorDurationScale(context) == 0f,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ABoxThemeContent(
|
||||
preferences: UiPreferences,
|
||||
systemInDarkTheme: Boolean,
|
||||
sdkInt: Int,
|
||||
dynamicColorTokens: ABoxColorTokens?,
|
||||
reducedMotion: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val resolution = ABoxThemeResolver.resolve(
|
||||
preferences = preferences,
|
||||
systemInDarkTheme = systemInDarkTheme,
|
||||
sdkInt = sdkInt,
|
||||
dynamicColorTokens = dynamicColorTokens,
|
||||
)
|
||||
val renderer = ABoxRendererAdapters.forRenderer(resolution.renderer)
|
||||
|
||||
@@ -65,7 +85,7 @@ fun ABoxTheme(
|
||||
LocalABoxTypography provides ABoxTypography,
|
||||
LocalABoxShapes provides resolution.shapeTokens,
|
||||
LocalABoxRenderer provides resolution.renderer,
|
||||
LocalABoxReducedMotion provides false,
|
||||
LocalABoxReducedMotion provides reducedMotion,
|
||||
LocalABoxBlurEnabled provides resolution.blurEnabled,
|
||||
) {
|
||||
renderer.Render(
|
||||
@@ -75,6 +95,16 @@ fun ABoxTheme(
|
||||
}
|
||||
}
|
||||
|
||||
private fun systemAnimatorDurationScale(context: android.content.Context): Float =
|
||||
runCatching {
|
||||
Settings.Global.getFloat(
|
||||
context.contentResolver,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f,
|
||||
)
|
||||
}.getOrDefault(1f)
|
||||
|
||||
|
||||
@Composable
|
||||
fun ABoxTheme(
|
||||
darkTheme: Boolean = androidx.compose.foundation.isSystemInDarkTheme(),
|
||||
|
||||
Reference in New Issue
Block a user