Skip to content

Commit 2c43f0f

Browse files
feat(BREAKING): allow click passthrough to underlying UI components (#253)
* feat(wip): allow passthrough of touch events on revealable items * fix: test * fix: update API dump * fix: passthrough issue (#252) * fixed passthrough issue * fixed hide revealable on touch outside in passthrough mode --------- Co-authored-by: Sven Jacobs <github@svenjacobs.com> * fix: formatting * fix: revert demo app * chore: update doc * fix: also consume down event * fix: ui tests (#255) * fix: handling of down event --------- Co-authored-by: Oleksandr Semenov <asemenovboyarka@gmail.com>
1 parent 043ab56 commit 2c43f0f

File tree

11 files changed

+250
-132
lines changed

11 files changed

+250
-132
lines changed

android-tests/src/androidTest/kotlin/com/svenjacobs/reveal/android/tests/BaseRevealTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.svenjacobs.reveal.android.tests
22

3+
import androidx.compose.foundation.layout.fillMaxSize
34
import androidx.compose.material3.Text
45
import androidx.compose.runtime.rememberCoroutineScope
56
import androidx.compose.ui.Modifier
@@ -41,6 +42,8 @@ abstract class BaseRevealTest {
4142

4243
RevealCanvas(revealCanvasState = revealCanvasState) {
4344
Reveal(
45+
// this must take full screen for correct clicks handling by test runner
46+
modifier = Modifier.fillMaxSize(),
4447
onRevealableClick = onRevealableClick,
4548
onOverlayClick = onOverlayClick,
4649
revealCanvasState = revealCanvasState,

android-tests/src/androidTest/kotlin/com/svenjacobs/reveal/android/tests/RevealTest.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class RevealTest : BaseRevealTest() {
1919
) { testRule, revealState, scope ->
2020
scope.launch { revealState.reveal(Keys.Key1) }
2121

22-
testRule.onNodeWithText("Overlay1").performClick()
22+
testRule.onNodeWithText("Overlay1")
23+
.assertExists()
24+
.performClick()
2325

2426
assertEquals(Keys.Key1, onRevealableClickKey)
2527
}
@@ -34,7 +36,9 @@ class RevealTest : BaseRevealTest() {
3436
) { testRule, revealState, scope ->
3537
scope.launch { revealState.reveal(Keys.Key1) }
3638

37-
testRule.onNodeWithTag("overlay").performClick()
39+
testRule.onNodeWithTag("overlay")
40+
.assertExists()
41+
.performClick()
3842

3943
assertEquals(Keys.Key1, onOverlayClickKey)
4044
}

android-tests/src/main/kotlin/com/svenjacobs/reveal/android/tests/presentation/MainScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Color
2222
import androidx.compose.ui.text.style.TextAlign
2323
import androidx.compose.ui.unit.dp
2424
import com.svenjacobs.reveal.Key
25+
import com.svenjacobs.reveal.OnClick
2526
import com.svenjacobs.reveal.Reveal
2627
import com.svenjacobs.reveal.RevealCanvasState
2728
import com.svenjacobs.reveal.RevealOverlayArrangement
@@ -68,7 +69,7 @@ fun MainScreen(revealCanvasState: RevealCanvasState, modifier: Modifier = Modifi
6869
key = Keys.Fab,
6970
shape = RevealShape.RoundRect(16.dp),
7071
borderStroke = BorderStroke(2.dp, Color.DarkGray),
71-
onClick = {
72+
onClick = OnClick.Listener {
7273
scope.launch { revealState.reveal(Keys.Explanation) }
7374
},
7475
),
@@ -96,7 +97,7 @@ fun MainScreen(revealCanvasState: RevealCanvasState, modifier: Modifier = Modifi
9697
.revealable(
9798
key = Keys.Explanation,
9899
borderStroke = BorderStroke(2.dp, Color.DarkGray),
99-
onClick = {
100+
onClick = OnClick.Listener {
100101
scope.launch { revealState.hide() }
101102
},
102103
),

demo-app/shared/src/commonMain/kotlin/com/svenjacobs/reveal/demo/presentation/MainScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Color
2222
import androidx.compose.ui.text.style.TextAlign
2323
import androidx.compose.ui.unit.dp
2424
import com.svenjacobs.reveal.Key
25+
import com.svenjacobs.reveal.OnClick
2526
import com.svenjacobs.reveal.Reveal
2627
import com.svenjacobs.reveal.RevealCanvasState
2728
import com.svenjacobs.reveal.RevealOverlayArrangement
@@ -68,7 +69,7 @@ fun MainScreen(revealCanvasState: RevealCanvasState, modifier: Modifier = Modifi
6869
key = Keys.Fab,
6970
shape = RevealShape.RoundRect(16.dp),
7071
borderStroke = BorderStroke(2.dp, Color.DarkGray),
71-
onClick = {
72+
onClick = OnClick.Listener {
7273
scope.launch { revealState.reveal(Keys.Explanation) }
7374
},
7475
),
@@ -96,7 +97,7 @@ fun MainScreen(revealCanvasState: RevealCanvasState, modifier: Modifier = Modifi
9697
.revealable(
9798
key = Keys.Explanation,
9899
borderStroke = BorderStroke(2.dp, Color.DarkGray),
99-
onClick = {
100+
onClick = OnClick.Listener {
100101
scope.launch { revealState.hide() }
101102
},
102103
),

reveal-core/api/android/reveal-core.api

Lines changed: 48 additions & 25 deletions
Large diffs are not rendered by default.

reveal-core/api/desktop/reveal-core.api

Lines changed: 48 additions & 25 deletions
Large diffs are not rendered by default.

reveal-core/api/reveal-core.klib.api

Lines changed: 39 additions & 14 deletions
Large diffs are not rendered by default.

reveal-core/src/commonMain/kotlin/com/svenjacobs/reveal/Modifiers.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,28 @@ package com.svenjacobs.reveal
33
import androidx.compose.foundation.BorderStroke
44
import androidx.compose.foundation.layout.PaddingValues
55
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.Immutable
67
import androidx.compose.ui.Modifier
78
import androidx.compose.ui.composed
89
import androidx.compose.ui.layout.onGloballyPositioned
910
import androidx.compose.ui.layout.positionInRoot
1011
import androidx.compose.ui.unit.dp
1112
import androidx.compose.ui.unit.toSize
1213

14+
public sealed interface OnClick {
15+
/**
16+
* Clicks on Revealable are handled by the specified handler.
17+
*/
18+
@Immutable
19+
public data class Listener(val listener: OnClickListener) : OnClick
20+
21+
/**
22+
* Clicks on Revealable are not handled by Reveal and passed through to underlying
23+
* composables.
24+
*/
25+
public data object Passthrough : OnClick
26+
}
27+
1328
/**
1429
* Registers the element as a revealable item.
1530
*
@@ -27,9 +42,10 @@ import androidx.compose.ui.unit.toSize
2742
* @param padding Additional padding around the reveal area. Positive values increase area
2843
* while negative values decrease it. Defaults to 8 dp on all sides.
2944
* @param borderStroke Optional border around the revealable item.
30-
* @param onClick Called when item is clicked while revealed. `key` is the key of this, the
31-
* clicked element. If click listener is defined here, clicks for this element
32-
* will not be handled by `onRevealableClick` of `Reveal`.
45+
* @param onClick If `null` clicks will be handled by `onRevealableClick` of `Reveal`.
46+
* If set to `OnClick.Listener` clicks will be handled by this listener.
47+
* If set to `OnClick.Passthrough` Reveal will not intercept clicks and clicks
48+
* will be passed through to underlying composables.
3349
*
3450
* @see Key
3551
*/
@@ -39,7 +55,7 @@ public fun Modifier.revealable(
3955
shape: RevealShape = RevealShape.RoundRect(4.dp),
4056
padding: PaddingValues = PaddingValues(8.dp),
4157
borderStroke: BorderStroke? = null,
42-
onClick: OnClickListener? = null,
58+
onClick: OnClick? = null,
4359
): Modifier = this.then(
4460
Modifier.revealable(
4561
state = state,
@@ -68,9 +84,10 @@ public fun Modifier.revealable(
6884
* @param padding Additional padding around the reveal area. Positive values increase area
6985
* while negative values decrease it. Defaults to 8 dp on all sides.
7086
* @param borderStroke Optional border around the revealable item.
71-
* @param onClick Called when item is clicked while revealed. `key` is the key of this, the
72-
* clicked element. If click listener is defined here, clicks for this element
73-
* will not be handled by `onRevealableClick` of `Reveal`.
87+
* @param onClick If `null` clicks will be handled by `onRevealableClick` of `Reveal`.
88+
* If set to `OnClick.Listener` clicks will be handled by this listener.
89+
* If set to `OnClick.Passthrough` Reveal will not intercept clicks and clicks
90+
* will be passed through to underlying composables.
7491
*
7592
* @see Key
7693
*/
@@ -80,7 +97,7 @@ public fun Modifier.revealable(
8097
shape: RevealShape = RevealShape.RoundRect(4.dp),
8198
padding: PaddingValues = PaddingValues(8.dp),
8299
borderStroke: BorderStroke? = null,
83-
onClick: OnClickListener? = null,
100+
onClick: OnClick? = null,
84101
): Modifier = this.then(
85102
Modifier.revealable(
86103
state = state,
@@ -109,9 +126,10 @@ public fun Modifier.revealable(
109126
* @param padding Additional padding around the reveal area. Positive values increase area
110127
* while negative values decrease it. Defaults to 8 dp on all sides.
111128
* @param borderStroke Optional border around the revealable item.
112-
* @param onClick Called when item is clicked while revealed. `key` is the key of this, the
113-
* clicked element. If click listener is defined here, clicks for this element
114-
* will not be handled by `onRevealableClick` of `Reveal`.
129+
* @param onClick If `null` clicks will be handled by `onRevealableClick` of `Reveal`.
130+
* If set to `OnClick.Listener` clicks will be handled by this listener.
131+
* If set to `OnClick.Passthrough` Reveal will not intercept clicks and clicks
132+
* will be passed through to underlying composables.
115133
*
116134
* @see Key
117135
*/
@@ -121,7 +139,7 @@ public fun Modifier.revealable(
121139
shape: RevealShape = RevealShape.RoundRect(4.dp),
122140
padding: PaddingValues = PaddingValues(8.dp),
123141
borderStroke: BorderStroke? = null,
124-
onClick: OnClickListener? = null,
142+
onClick: OnClick? = null,
125143
): Modifier = this.then(
126144
Modifier
127145
.onGloballyPositioned { layoutCoordinates ->

reveal-core/src/commonMain/kotlin/com/svenjacobs/reveal/Reveal.kt

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.svenjacobs.reveal
22

33
import androidx.compose.animation.core.animateFloatAsState
4-
import androidx.compose.foundation.gestures.detectTapGestures
4+
import androidx.compose.foundation.gestures.awaitEachGesture
5+
import androidx.compose.foundation.gestures.awaitFirstDown
6+
import androidx.compose.foundation.gestures.waitForUpOrCancellation
57
import androidx.compose.foundation.layout.Box
68
import androidx.compose.foundation.layout.fillMaxSize
79
import androidx.compose.runtime.Composable
@@ -13,6 +15,7 @@ import androidx.compose.runtime.remember
1315
import androidx.compose.runtime.rememberUpdatedState
1416
import androidx.compose.ui.Modifier
1517
import androidx.compose.ui.draw.alpha
18+
import androidx.compose.ui.input.pointer.PointerEventPass
1619
import androidx.compose.ui.input.pointer.pointerInput
1720
import androidx.compose.ui.platform.LocalDensity
1821
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -94,21 +97,56 @@ public fun Reveal(
9497
val layoutDirection = LocalLayoutDirection.current
9598
val density = LocalDensity.current
9699

97-
Box(
98-
modifier = modifier,
99-
) {
100-
content(RevealScopeInstance(revealState))
100+
val currentRevealable = remember {
101+
derivedStateOf {
102+
revealState.currentRevealable?.toActual(
103+
density = density,
104+
layoutDirection = layoutDirection,
105+
additionalOffset = revealCanvasState.revealableOffset,
106+
)
107+
}
108+
}
101109

102-
val currentRevealable = remember {
103-
derivedStateOf {
104-
revealState.currentRevealable?.toActual(
105-
density = density,
106-
layoutDirection = layoutDirection,
107-
additionalOffset = revealCanvasState.revealableOffset,
110+
val rev by rememberUpdatedState(currentRevealable.value)
111+
112+
val clickModifier = when {
113+
revealState.isVisible -> Modifier.pointerInput(Unit) {
114+
awaitEachGesture {
115+
val down = awaitFirstDown(pass = PointerEventPass.Initial)
116+
if (rev?.onClick !is OnClick.Passthrough) {
117+
down.consume()
118+
}
119+
120+
val up = waitForUpOrCancellation(pass = PointerEventPass.Initial)
121+
?: return@awaitEachGesture
122+
123+
rev?.key?.let(
124+
if (rev?.area?.contains(up.position) == true) {
125+
// pass through touches in the area on top of revealable
126+
if (rev?.onClick is OnClick.Passthrough) {
127+
return@awaitEachGesture
128+
} else {
129+
(rev?.onClick as? OnClick.Listener)?.listener ?: onRevealableClick
130+
}
131+
} else {
132+
onOverlayClick
133+
},
108134
)
135+
136+
up.consume()
109137
}
110138
}
111139

140+
else -> Modifier
141+
}
142+
143+
Box(
144+
modifier = modifier
145+
.then(clickModifier)
146+
.semantics { testTag = "overlay" },
147+
) {
148+
content(RevealScopeInstance(revealState))
149+
112150
val previousRevealable = remember {
113151
derivedStateOf {
114152
revealState.previousRevealable?.toActual(
@@ -119,26 +157,6 @@ public fun Reveal(
119157
}
120158
}
121159

122-
val rev by rememberUpdatedState(currentRevealable.value)
123-
124-
val clickModifier = when {
125-
revealState.isVisible -> Modifier.pointerInput(Unit) {
126-
detectTapGestures(
127-
onPress = { offset ->
128-
rev?.key?.let(
129-
if (rev?.area?.contains(offset) == true) {
130-
rev?.onClick ?: onRevealableClick
131-
} else {
132-
onOverlayClick
133-
},
134-
)
135-
},
136-
)
137-
}
138-
139-
else -> Modifier
140-
}
141-
142160
LaunchedEffect(animatedOverlayAlpha) {
143161
@Suppress("ktlint:standard:wrapping")
144162
revealCanvasState.overlayContent = when {
@@ -147,8 +165,7 @@ public fun Reveal(
147165
revealState = revealState,
148166
currentRevealable = currentRevealable,
149167
previousRevealable = previousRevealable,
150-
modifier = clickModifier
151-
.semantics { testTag = "overlay" }
168+
modifier = Modifier
152169
.fillMaxSize()
153170
.alpha(animatedOverlayAlpha),
154171
content = overlayContent,

0 commit comments

Comments
 (0)