-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #489 from tuanchauict/ui/improve-dropdown
Rewrite Dropdown menu in compose
- Loading branch information
Showing
8 changed files
with
273 additions
and
206 deletions.
There are no files selected for viewing
109 changes: 0 additions & 109 deletions
109
libs/ui-modal/src/main/kotlin/mono/html/modal/DropDownMenu.kt
This file was deleted.
Oops, something went wrong.
88 changes: 88 additions & 0 deletions
88
libs/ui-modal/src/main/kotlin/mono/html/modal/compose/DropDownMenu.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* Copyright (c) 2023, tuanchauict | ||
*/ | ||
|
||
@file:Suppress("FunctionName") | ||
|
||
package mono.html.modal.compose | ||
|
||
import androidx.compose.runtime.Composable | ||
import kotlinx.browser.document | ||
import mono.html.px | ||
import mono.html.style | ||
import org.jetbrains.compose.web.dom.Li | ||
import org.jetbrains.compose.web.dom.Text | ||
import org.jetbrains.compose.web.dom.Ul | ||
import org.w3c.dom.Element | ||
|
||
/** | ||
* Show drop down menu at the bottom of the anchor, horizontal center aligned with the anchor. | ||
*/ | ||
fun DropDownMenu( | ||
anchor: Element, | ||
items: List<DropDownItem>, | ||
onClick: (DropDownItem) -> Unit | ||
) { | ||
NoBackgroundModal( | ||
attrs = { | ||
classes("drop-down-menu") | ||
|
||
ref { | ||
it.adjustPosition(anchor) | ||
onDispose { } | ||
} | ||
} | ||
) { | ||
Ul { | ||
for (item in items) { | ||
if (!item.isVisible()) { | ||
continue | ||
} | ||
when (item) { | ||
is DropDownItem.Divider -> Divider() | ||
is DropDownItem.Text -> Item(item) { | ||
onClick(it) | ||
dismiss() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun Divider() { | ||
Li(attrs = { classes("drop-down-divider") }) | ||
} | ||
|
||
@Composable | ||
private fun Item(item: DropDownItem.Text, onClick: (DropDownItem) -> Unit) { | ||
Li( | ||
attrs = { | ||
classes("drop-down-item") | ||
onClick { onClick(item) } | ||
} | ||
) { | ||
Text(item.title) | ||
} | ||
} | ||
|
||
private fun Element.adjustPosition(anchor: Element) { | ||
val maxWidth = document.body?.clientWidth ?: return | ||
val menuWidthPx = clientWidth | ||
val anchorRect = anchor.getBoundingClientRect() | ||
|
||
val leftPx = | ||
(anchorRect.left + anchorRect.width / 2 - menuWidthPx / 2).toInt() | ||
.coerceIn(4, maxWidth - menuWidthPx - 4) | ||
style( | ||
"left" to leftPx.px, | ||
"top" to anchorRect.bottom.px | ||
) | ||
} | ||
|
||
sealed class DropDownItem(internal val isVisible: () -> Boolean) { | ||
class Divider(isVisible: () -> Boolean = { true }) : DropDownItem(isVisible) | ||
class Text(val title: String, val key: Any, isVisible: () -> Boolean = { true }) : | ||
DropDownItem(isVisible) | ||
} |
16 changes: 16 additions & 0 deletions
16
libs/ui-modal/src/main/kotlin/mono/html/modal/compose/ModalElementScope.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/* | ||
* Copyright (c) 2023, tuanchauict | ||
*/ | ||
|
||
package mono.html.modal.compose | ||
|
||
import org.jetbrains.compose.web.dom.ElementScope | ||
import org.w3c.dom.HTMLDivElement | ||
|
||
/** | ||
* A [ElementScope] for modal, which has extra APIs for communicating with content of the modal. | ||
*/ | ||
class ModalElementScope( | ||
private val elementScope: ElementScope<HTMLDivElement>, | ||
val dismiss: () -> Unit | ||
) : ElementScope<HTMLDivElement> by elementScope |
105 changes: 105 additions & 0 deletions
105
libs/ui-modal/src/main/kotlin/mono/html/modal/compose/NoBackgroundModal.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Copyright (c) 2023, tuanchauict | ||
*/ | ||
|
||
@file:Suppress("FunctionName") | ||
|
||
package mono.html.modal.compose | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import kotlinx.browser.document | ||
import mono.common.Cancelable | ||
import mono.common.setTimeout | ||
import mono.html.Div | ||
import org.jetbrains.compose.web.css.Position | ||
import org.jetbrains.compose.web.css.height | ||
import org.jetbrains.compose.web.css.left | ||
import org.jetbrains.compose.web.css.position | ||
import org.jetbrains.compose.web.css.px | ||
import org.jetbrains.compose.web.css.width | ||
import org.jetbrains.compose.web.dom.AttrBuilderContext | ||
import org.jetbrains.compose.web.dom.CheckboxInput | ||
import org.jetbrains.compose.web.dom.Div | ||
import org.jetbrains.compose.web.renderComposable | ||
import org.w3c.dom.HTMLDivElement | ||
|
||
/** | ||
* Show a modal without background | ||
*/ | ||
internal fun NoBackgroundModal( | ||
attrs: AttrBuilderContext<HTMLDivElement>, | ||
onDismiss: () -> Unit = {}, | ||
content: @Composable ModalElementScope.() -> Unit | ||
) { | ||
val body = document.body ?: return | ||
val container = body.Div() | ||
val composition = renderComposable(container) {} | ||
composition.setContent { | ||
var isDismissed by remember { mutableStateOf(false) } | ||
ModalContainer(attrs, content) { | ||
if (!isDismissed) { | ||
composition.dispose() | ||
container.remove() | ||
onDismiss() | ||
isDismissed = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ModalContainer( | ||
attrs: AttrBuilderContext<HTMLDivElement>, | ||
content: @Composable ModalElementScope.() -> Unit, | ||
dismiss: () -> Unit | ||
) { | ||
var cancelable: Cancelable? by remember { mutableStateOf(null) } | ||
Div( | ||
attrs = { | ||
classes("no-background-modal") | ||
tabIndex(-1) | ||
|
||
onFocusIn { cancelable?.cancel() } | ||
|
||
onFocusOut { | ||
if (document.hasFocus()) { | ||
cancelable = setTimeout(20) { | ||
dismiss() | ||
} | ||
} | ||
} | ||
|
||
onKeyDown { | ||
when (it.key) { | ||
"Escape" -> dismiss() | ||
// TODO: Use ArrowDown and ArrowUp for changing the active project | ||
// TODO: Use Enter for opening the project by keyboard | ||
} | ||
} | ||
|
||
attrs.invoke(this) | ||
} | ||
) { | ||
// Hidden input for making the modal focused by default. | ||
// Making the modal focused by default can make the input of the modal not being focused | ||
// by default. | ||
CheckboxInput { | ||
style { | ||
position(Position.Fixed) | ||
left((-1000).px) | ||
width(0.px) | ||
height(0.px) | ||
} | ||
ref { | ||
it.focus() | ||
onDispose { } | ||
} | ||
} | ||
val scope = ModalElementScope(this, dismiss) | ||
content.invoke(scope) | ||
} | ||
} |
Oops, something went wrong.