@@ -26,6 +26,7 @@ package com.composeunstyled
2626import androidx.compose.foundation.Indication
2727import androidx.compose.foundation.LocalIndication
2828import androidx.compose.foundation.focusGroup
29+ import androidx.compose.foundation.focusable
2930import androidx.compose.foundation.gestures.Orientation
3031import androidx.compose.foundation.interaction.MutableInteractionSource
3132import androidx.compose.foundation.layout.Arrangement
@@ -40,13 +41,11 @@ import androidx.compose.foundation.layout.padding
4041import androidx.compose.foundation.selection.selectable
4142import androidx.compose.foundation.selection.selectableGroup
4243import androidx.compose.runtime.Composable
43- import androidx.compose.runtime.CompositionLocalProvider
4444import androidx.compose.runtime.SideEffect
4545import androidx.compose.runtime.getValue
4646import androidx.compose.runtime.mutableStateOf
4747import androidx.compose.runtime.remember
4848import androidx.compose.runtime.setValue
49- import androidx.compose.runtime.staticCompositionLocalOf
5049import androidx.compose.ui.Alignment
5150import androidx.compose.ui.Modifier
5251import androidx.compose.ui.focus.FocusDirection
@@ -71,28 +70,44 @@ private val NoPadding = PaddingValues(0.dp)
7170private val KeyEvent .isKeyDown: Boolean
7271 get() = type == KeyEventType .KeyDown
7372
74- private class TabsRegistry {
75- var focusedTab: TabKey ? by mutableStateOf(null )
76- var activatedTab: TabKey ? by mutableStateOf(null )
73+ internal class TabsRegistry <T >(
74+ val onSelectedTabChange : (T ) -> Unit = {},
75+ ) {
76+ var focusedTab: T ? by mutableStateOf(null )
77+ var activatedTab: T ? by mutableStateOf(null )
7778
78- var tabKeys: List <TabKey > by mutableStateOf(emptyList())
79- var tabFocusRequesters: Map <TabKey , FocusRequester > by mutableStateOf(emptyMap())
80- var panelsFocusRequesters: Map <TabKey , FocusRequester > by mutableStateOf(emptyMap())
79+ var tabKeys: List <T > by mutableStateOf(emptyList())
80+ var tabFocusRequesters: Map <T , FocusRequester > by mutableStateOf(emptyMap())
81+ var panelsFocusRequesters: Map <T , FocusRequester > by mutableStateOf(emptyMap())
8182}
8283
83- private val LocalTabsRegistry = staticCompositionLocalOf { TabsRegistry () }
84+ class TabGroupScope <T > internal constructor(
85+ internal val registry : TabsRegistry <T >,
86+ columnScope : ColumnScope ,
87+ ) : ColumnScope by columnScope
88+
89+ class TabListScope <T > internal constructor(
90+ internal val registry : TabsRegistry <T >,
91+ rowScope : RowScope ,
92+ ) : RowScope by rowScope
93+
94+ class TabScope internal constructor(
95+ val selected : Boolean ,
96+ val enabled : Boolean ,
97+ )
8498
8599@Composable
86- fun UnstyledTabGroup (
87- selectedTab : TabKey ,
88- tabs : List <TabKey >,
100+ fun <T > UnstyledTabGroup (
101+ selectedTab : T ,
102+ onSelectedTabChange : (T ) -> Unit ,
103+ tabs : List <T >,
89104 modifier : Modifier = Modifier ,
90- content : @Composable ColumnScope .() -> Unit ,
105+ content : @Composable TabGroupScope < T > .() -> Unit ,
91106) {
92- val registry = remember(tabs) {
107+ val registry = remember(tabs, onSelectedTabChange ) {
93108 val tabsRequesters = tabs.associateWith { FocusRequester () }
94109 val panelsRequesters = tabs.associateWith { FocusRequester () }
95- TabsRegistry ().apply {
110+ TabsRegistry (onSelectedTabChange = onSelectedTabChange ).apply {
96111 tabKeys = tabs
97112 tabFocusRequesters = tabsRequesters
98113 panelsFocusRequesters = panelsRequesters
@@ -116,22 +131,23 @@ fun UnstyledTabGroup(
116131 }
117132 .focusGroup(),
118133 ) {
119- CompositionLocalProvider ( LocalTabsRegistry provides registry) {
120- content( )
134+ val tabGroupScope = remember( registry, this ) {
135+ TabGroupScope (registry, this )
121136 }
137+ tabGroupScope.content()
122138 }
123139}
124140
125141@Composable
126- fun UnstyledTabList (
142+ fun < T > TabGroupScope<T>. TabList (
127143 modifier : Modifier = Modifier ,
128144 contentPadding : PaddingValues = NoPadding ,
129145 orientation : Orientation = Orientation .Horizontal ,
130146 horizontalArrangement : Arrangement .Horizontal = Arrangement .Start ,
131147 verticalAlignment : Alignment .Vertical = Alignment .Top ,
132- content : @Composable RowScope .() -> Unit ,
148+ content : @Composable TabListScope < T > .() -> Unit ,
133149) {
134- val registry = LocalTabsRegistry .current
150+ val registry = registry
135151 val tabKeys = registry.tabKeys
136152
137153 Row (
@@ -143,7 +159,7 @@ fun UnstyledTabList(
143159 .onKeyEvent { event ->
144160 when {
145161 orientation == Orientation .Horizontal && event.key == Key .DirectionLeft -> {
146- if (event.isKeyDown) {
162+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
147163 val currentIndex = tabKeys.indexOf(registry.focusedTab)
148164
149165 val nextIndex = (currentIndex - 1 + tabKeys.size) % tabKeys.size
@@ -156,7 +172,7 @@ fun UnstyledTabList(
156172 }
157173
158174 orientation == Orientation .Horizontal && event.key == Key .DirectionRight -> {
159- if (event.isKeyDown) {
175+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
160176 val currentIndex = tabKeys.indexOf(registry.focusedTab)
161177
162178 val nextIndex = (currentIndex + 1 ) % tabKeys.size
@@ -169,7 +185,7 @@ fun UnstyledTabList(
169185 }
170186
171187 orientation == Orientation .Vertical && event.key == Key .DirectionUp -> {
172- if (event.isKeyDown) {
188+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
173189 val currentIndex = tabKeys.indexOf(registry.focusedTab)
174190
175191 val nextIndex = (currentIndex - 1 + tabKeys.size) % tabKeys.size
@@ -182,7 +198,7 @@ fun UnstyledTabList(
182198 }
183199
184200 orientation == Orientation .Vertical && event.key == Key .DirectionDown -> {
185- if (event.isKeyDown) {
201+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
186202 val currentIndex = tabKeys.indexOf(registry.focusedTab)
187203
188204 val nextIndex = (currentIndex + 1 ) % tabKeys.size
@@ -195,7 +211,7 @@ fun UnstyledTabList(
195211 }
196212
197213 event.key == Key .Home -> {
198- if (event.isKeyDown) {
214+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
199215 val tab = tabKeys.first()
200216 val focusRequester = registry.tabFocusRequesters.getValue(tab)
201217 focusRequester.requestFocus()
@@ -204,7 +220,7 @@ fun UnstyledTabList(
204220 }
205221
206222 event.key == Key .MoveEnd -> {
207- if (event.isKeyDown) {
223+ if (event.isKeyDown && tabKeys.isNotEmpty() ) {
208224 val tab = tabKeys.last()
209225 val focusRequester = registry.tabFocusRequesters.getValue(tab)
210226 focusRequester.requestFocus()
@@ -223,27 +239,34 @@ fun UnstyledTabList(
223239 horizontalArrangement = horizontalArrangement,
224240 verticalAlignment = verticalAlignment,
225241 ) {
226- content()
242+ val tabListScope = remember(registry, this ) {
243+ TabListScope (registry, this )
244+ }
245+ tabListScope.content()
227246 }
228247}
229248
230249@Composable
231- fun UnstyledTab (
232- key : TabKey ,
233- selected : Boolean ,
234- onSelected : () -> Unit ,
250+ fun <T > TabListScope<T>.Tab (
251+ key : T ,
235252 modifier : Modifier = Modifier ,
236253 enabled : Boolean = true,
237254 activateOnFocus : Boolean = true,
238- indication : Indication = LocalIndication .current,
255+ indication : Indication ? = LocalIndication .current,
239256 interactionSource : MutableInteractionSource ? = null,
240257 contentPadding : PaddingValues = NoPadding ,
241- content : @Composable () -> Unit ,
258+ content : @Composable TabScope . () -> Unit ,
242259) {
243- val registry = LocalTabsRegistry .current
244- val focusRequester = registry.tabFocusRequesters[key]
245- ? : error(" Tried to setup a Tab with key = $key but was not found in the tab keys. Make sure you provide the key" )
260+ val registry = registry
261+ val focusRequester = registry.tabFocusRequesters[key] ? : FocusRequester .Default
246262 val activatedTab = registry.activatedTab
263+ val selected = activatedTab == key
264+ val tabScope = remember(selected, enabled) {
265+ TabScope (
266+ selected = selected,
267+ enabled = enabled,
268+ )
269+ }
247270
248271 Box (
249272 modifier = modifier
@@ -255,42 +278,42 @@ fun UnstyledTab(
255278 if (it.isFocused) {
256279 registry.focusedTab = key
257280 if (activateOnFocus) {
258- onSelected( )
281+ registry.onSelectedTabChange(key )
259282 }
260283 }
261284 }
262285 .selectable(
263286 selected = selected,
264- onClick = onSelected ,
287+ onClick = { registry.onSelectedTabChange(key) } ,
265288 indication = indication,
266289 interactionSource = interactionSource,
267290 enabled = enabled,
268291 role = Role .Tab ,
269292 )
270293 .padding(contentPadding),
271294 ) {
272- content()
295+ tabScope. content()
273296 }
274297}
275298
276299@Composable
277- fun UnstyledTabPanel (
278- key : TabKey ,
300+ fun < T > TabGroupScope<T>. TabPanel (
301+ key : T ,
279302 modifier : Modifier = Modifier ,
280303 contentPadding : PaddingValues = NoPadding ,
281304 contentAlignment : Alignment = Alignment .TopStart ,
282305 content : @Composable BoxScope .() -> Unit ,
283306) {
284- val registry = LocalTabsRegistry .current
307+ val registry = registry
285308
286309 if (registry.activatedTab == key) {
287- val focusRequester = registry.panelsFocusRequesters[key]
288- ? : error(" Tried to activate TabPanel with key = $key . Did you forget to pass the key in the list of tabs in your TabGroup?" )
310+ val focusRequester = registry.panelsFocusRequesters[key] ? : FocusRequester .Default
289311
290312 Box (
291313 modifier = modifier
292314 .padding(contentPadding)
293315 .focusRequester(focusRequester)
316+ .focusable()
294317 .focusGroup(),
295318 contentAlignment = contentAlignment,
296319 ) {
0 commit comments