diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt index ad2e67dbc7..a66ea7b391 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt @@ -67,6 +67,7 @@ fun DesktopNoticeView( title = stringResource(R.string.screen_link_new_device_desktop_title, appName), iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()), modifier = modifier, + isScrollable = true, buttons = { Button( text = stringResource(R.string.screen_link_new_device_desktop_submit), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt index df841dad9b..03a5258b78 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt @@ -9,8 +9,7 @@ package io.element.android.libraries.designsystem.atomic.organisms import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString @@ -23,11 +22,11 @@ fun NumberedListOrganism( items: ImmutableList, modifier: Modifier = Modifier, ) { - LazyColumn( + Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(24.dp), ) { - itemsIndexed(items) { index, item -> + for ((index, item) in items.withIndex()) { NumberedListMolecule(index = index + 1, text = item) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterLayout.kt new file mode 100644 index 0000000000..af707ec251 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterLayout.kt @@ -0,0 +1,106 @@ +/* + * 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.atomic.pages + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +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.layout.Layout +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Constraints +import kotlin.math.max + +/** + * A layout with a [header], a [footer] and a [content] composables. + * The component can be scrollable or not, which will modify which layout is used internally to arrange the UI. + * In both cases, the [content] will use all the available space between the [header] and the [footer]. + * The [footer] is always visible, the [header] and the [content] can be scrolled if [isScrollable] is true. + * + * @param header the header composable. + * @param footer the footer composable. + * @param isScrollable if the content should be scrollable. + * @param contentInsetsPadding padding values to apply to the content. This is useful to handle system insets. + * @param modifier Compose modifier. + * @param content the main content composable. + */ +@Suppress("ContentSlotReused") +@Composable +fun HeaderFooterLayout( + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + isScrollable: Boolean, + contentInsetsPadding: PaddingValues, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + if (isScrollable) { + // This is a hack to make the content at least 1px high, otherwise the layout will crash + var newHeight by remember { mutableIntStateOf(1) } + Box( + modifier = Modifier + .padding(contentInsetsPadding) + .weight(1f, fill = true) + .onSizeChanged { + newHeight = it.height + } + ) { + Layout( + content = { + header() + content() + }, + modifier = Modifier.verticalScroll(rememberScrollState()), + measurePolicy = { measurables, constraints -> + val actualConstraints = constraints.copy(minWidth = 0, minHeight = 0, maxHeight = newHeight) + val headerPlaceable = measurables.firstOrNull()?.measure(actualConstraints) + + val contentPlaceable = if (headerPlaceable != null && measurables.size > 1) { + val availableContentHeight = max(1, actualConstraints.maxHeight - headerPlaceable.height) + val contentConstraints = actualConstraints.copy(minHeight = availableContentHeight, maxHeight = Constraints.Infinity) + measurables[1].measure(contentConstraints) + } else { + null + } + + val headerHeight = headerPlaceable?.height ?: 0 + val contentHeight = contentPlaceable?.height ?: 0 + layout(actualConstraints.maxWidth, headerHeight + contentHeight) { + var yPosition = 0 + + headerPlaceable?.let { + it.placeRelative(0, yPosition) + yPosition += it.height + } + + contentPlaceable?.placeRelative(0, yPosition) + } + } + ) + } + } else { + Box(Modifier.padding(contentInsetsPadding)) { + header() + } + Box(Modifier.weight(1f, fill = true)) { + content() + } + } + + footer() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index f09dc895e9..5ca601baae 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -9,19 +9,14 @@ package io.element.android.libraries.designsystem.atomic.pages import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -82,48 +77,27 @@ fun HeaderFooterPage( Box { background() - // Render in a Column - Column( + HeaderFooterLayout( modifier = Modifier .fillMaxSize() .padding(paddingValues = contentPadding) .consumeWindowInsets(insetsPadding) .imePadding(), - ) { - // Content - Column( - modifier = Modifier - .fillMaxWidth() - .run { - if (isScrollable) { - verticalScroll(rememberScrollState()) - // Make sure the scrollable content takes the full available height - .height(IntrinsicSize.Max) - } else { - Modifier - } - } - // Apply insets here so if the content is scrollable it can get below the top app bar if needed - .padding(contentInsetsPadding) - .weight(1f, fill = true), - ) { - // Header - header() - Box { - content() + isScrollable = isScrollable, + contentInsetsPadding = contentInsetsPadding, + header = header, + content = content, + footer = { + Box( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + .fillMaxWidth() + .padding(footerInsetsPadding) + ) { + footer() } - } - - // Footer - Box( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, top = 16.dp) - .fillMaxWidth() - .padding(footerInsetsPadding) - ) { - footer() - } - } + }, + ) } } } diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png index 5bc0a7e57a..48598e410f 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da71297e7073c5c592dc154af7f137c4eb500bf528e1509b95da869a2c61cbd2 -size 30488 +oid sha256:5c79c83bde346bec65f67c34805407e46b351b2c91772c13e0a6c23ae09bf7e8 +size 30345 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png index d2ab2dcb1e..da85b85089 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48cafd6b98791b64e4cc6a16c602f17e3a886659698c3c93bc4acaf875337e0f -size 31102 +oid sha256:e666d896f715b1e6a165514acbe603fc879c0495a2d7215ac7cabe7a8d165e09 +size 30961 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en.png index fac34cc904..d22d7f98cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bbb94fdb717b2bcda3683d5833181c59d00e4c03a1860af66de6225c8df0756 -size 32482 +oid sha256:ff17ff66d5c92ab19ce8064e2e14326e9f2921c1b7313a3974547b2932cb7fb1 +size 32295 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png index c4593656d1..05d8692fb0 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f6f8d1282ae47a5240aec2b2c63a137b8e3e25ec8b746cf2d82d0fa7e0c0a34 -size 30287 +oid sha256:6a7786eecdd8f9f8ee2c7edb0d76a2fc09a960f6ca457445b35dc428539e5be6 +size 30102 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png index 3314252e26..ac8d5e8611 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e97cab70b9ee3e870154ad4ae4cf1bf0bb413facf8253f5f9fe9429eddf76b1 -size 60303 +oid sha256:d70106c0e4d800aa34faa380d79b65ac29c6bc43e41cf874f31517f138e252f9 +size 60126 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png index d58e079ced..4d93016dd4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:caaa2a0f4de455704a6b5c78e19c2f723be7deaeec470c77ff1e87df194dd95c -size 58620 +oid sha256:75670dcefa55cf863d2c0dd608704d465b5c9ac74f4798b3503ce9a2d8e74417 +size 58518 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png index 6bcc250b5c..e435ce728f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3874d6fa6ad92eac4ccfbbc2ea76cf8b1fbc869a725eb5e8131af52e78be7a4 -size 12427 +oid sha256:5d8630310d2c2f08aae23fe2e98b9b7b6dec253cbc0528804c999aa3d2d40658 +size 12439 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png index 5eaa6da79f..d816e772f4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60d3361163e9a846d0686e696ba25e35b6a8ca9b691d711ff0f5fdd3b1d5578d -size 12091 +oid sha256:99e36471b02eedc24bf1a07ffc720fe18ca5482e7fc0f35316ab407fd485cb34 +size 12105