Add shared PasswordVisibilityToggle design-system component

Extract the show/hide password trailing-icon into a reusable
PasswordVisibilityToggle so every password field reveals plaintext the
same way and announces the same a11y labels. Reuse it from the login and
reset-identity password fields. Pure refactor, no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jenna Vassar
2026-06-02 13:33:04 -07:00
parent 42566b4b1f
commit 748454f34d
3 changed files with 47 additions and 21 deletions
@@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.PasswordVisibilityToggle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@@ -249,16 +250,10 @@ private fun LoginForm(
placeholder = stringResource(CommonStrings.common_password),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
val description =
if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
Box(Modifier.clickable { passwordVisible = !passwordVisible }) {
Icon(
imageVector = image,
contentDescription = description,
)
}
PasswordVisibilityToggle(
visible = passwordVisible,
onToggle = { passwordVisible = !passwordVisible },
)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
@@ -8,8 +8,6 @@
package io.element.android.features.securebackup.impl.reset.password
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,7 +30,7 @@ import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusN
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.PasswordVisibilityToggle
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.ui.strings.CommonStrings
@@ -92,14 +90,10 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool
singleLine = true,
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
if (showPassword) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
val description =
if (showPassword) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
Box(Modifier.clickable { showPassword = !showPassword }) {
Icon(imageVector = image, description)
}
PasswordVisibilityToggle(
visible = showPassword,
onToggle = { showPassword = !showPassword },
)
},
validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None,
supportingText = if (hasError) {
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Show/hide toggle for a password [TextField], intended for its `trailingIcon` slot.
* Shared so every password field reveals plaintext the same way and announces the
* same accessibility labels.
*/
@Composable
fun PasswordVisibilityToggle(
visible: Boolean,
onToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier.clickable(onClick = onToggle)) {
Icon(
imageVector = if (visible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff(),
contentDescription = stringResource(
if (visible) CommonStrings.a11y_hide_password else CommonStrings.a11y_show_password
),
)
}
}