fix(month): fade the selection outline with the page it belongs to (#53)

Sending the outline to the shared-transition overlay stopped it hiding
behind its own cell, but it also lifted it out of the layer AnimatedContent
fades the outgoing page with — so it stopped fading at all. It stood at
full strength for the whole expand or collapse and then blinked out at the
end, which is the opposite half of the same bug.

The fade is reapplied on the lifted content's own layer, so it travels with
it. That belongs in morphOverlay rather than at the call site: anything
sent to the overlay loses the parent's fade the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:43:21 +02:00
parent bdb16d069e
commit 7fb01ab8d1

View File

@@ -8,6 +8,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
/** /**
@@ -85,14 +86,30 @@ internal fun Modifier.morphElement(key: MonthMorphKey): Modifier {
* The content keeps its own layout bounds; only where it is *painted* changes. So * The content keeps its own layout bounds; only where it is *painted* changes. So
* the outline still draws at the size the collapsed cell actually is, which is * the outline still draws at the size the collapsed cell actually is, which is
* the whole reason it was left untagged. * the whole reason it was left untagged.
*
* Lifting content out of the regular tree also lifts it out of the enter/exit
* layer [AnimatedContent][androidx.compose.animation.AnimatedContent] fades the
* outgoing page with, so it no longer fades at all — it stood at full strength
* for the whole transition and then blinked out at the end of it. The fade is
* therefore reapplied here, on the lifted content's own layer, so it travels with
* it. Anything sent to the overlay needs this, which is why it lives in the
* helper rather than at the call site.
*/ */
@OptIn(ExperimentalSharedTransitionApi::class) @OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
internal fun Modifier.morphOverlay(zIndex: Float = 1f): Modifier { internal fun Modifier.morphOverlay(zIndex: Float = 1f): Modifier {
val morph = LocalMonthMorph.current ?: return this val morph = LocalMonthMorph.current ?: return this
return with(morph.shared) { val fadeSpec = rememberCalendarFadeSpec()
val lifted = with(morph.shared) {
this@morphOverlay.renderInSharedTransitionScopeOverlay(zIndexInOverlay = zIndex) this@morphOverlay.renderInSharedTransitionScopeOverlay(zIndexInOverlay = zIndex)
} }
return with(morph.visibility) {
lifted.animateEnterExit(
enter = fadeIn(fadeSpec),
exit = fadeOut(fadeSpec),
label = "morph-overlay-fade",
)
}
} }
/** /**