Merge pull request 'feat(components): revealOnFocus' (#12) from feat/reveal-on-focus into main

Reviewed-on: https://codeberg.org/jlmakiola/floret-kit/pulls/12
This commit is contained in:
Jean-Luc Makiola committed 2026-10-09 12:04:21 +02:00
commit 0b44d353d2
4 files changed
+65 -3

No files matched your search

@@ -21,6 +21,6 @@ fun OptionalFormSection(visible: Boolean, content: @Composable ColumnScope.() ->
enter = expandEnter(),
exit = collapseExit(),
) {
Column(modifier = Modifier.fillMaxWidth(), content = content)
Column(modifier = Modifier.fillMaxWidth().revealOnFocus(), content = content)
}
}
@@ -122,7 +122,7 @@ fun GroupedSurface(
position == Position.Top || position == Position.Middle -> Modifier.padding(bottom = 2.dp)
else -> Modifier
}
val base = modifier.fillMaxWidth().then(gap)
val base = modifier.fillMaxWidth().then(gap).revealOnFocus()
val haptics = LocalHapticFeedback.current
when {
onLongClick != null -> Surface(
@@ -91,6 +91,6 @@ fun InlineTextField(
innerTextField()
}
},
modifier = modifier,
modifier = modifier.revealOnFocus(),
)
}
@@ -0,0 +1,62 @@
package de.jeanlucmakiola.floret.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
/**
* Scrolls this whole element into view while anything inside it has focus — once
* the keyboard has finished opening, and again whenever the element resizes.
*
* A text field on its own only brings its text line into view, so a card's
* padding, its label and whatever appears below the field (suggestions, chips)
* stay under the keyboard. [InlineTextField], [GroupedSurface] and
* [OptionalFormSection] apply it already; add it to an app's own card or row
* that holds an input. Needs a scrolling parent whose viewport shrinks with the
* keyboard (`imePadding`).
*
* The keyboard is only read while focused, so an unfocused element never
* recomposes as the keyboard animates.
*/
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
fun Modifier.revealOnFocus(): Modifier = composed {
val requester = remember { BringIntoViewRequester() }
var focused by remember { mutableStateOf(false) }
var size by remember { mutableStateOf(IntSize.Zero) }
val density = LocalDensity.current
val ime = WindowInsets.ime
val imeTarget = WindowInsets.imeAnimationTarget
LaunchedEffect(focused) {
if (!focused) return@LaunchedEffect
// Reveal against the final viewport, not one frame of the keyboard sliding in.
snapshotFlow {
val now = ime.getBottom(density)
if (now == imeTarget.getBottom(density)) now to size else null
}
.filterNotNull()
.distinctUntilChanged()
.collect { requester.bringIntoView() }
}
this
.bringIntoViewRequester(requester)
.onSizeChanged { size = it }
.onFocusChanged { focused = it.hasFocus }
}