Display popup dialog or modal #549
-
|
Hi team, |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
|
Silk does not currently have a modal widget (one day, we plan to!), but here's something I implemented myself in an old project. Feel free to check it out and modify it as needed for your case. (Silk does have an Here, my modal class is designed to allow a row of title elements that appear at the top, a row at the bottom (useful for buttons), and then the content area, which lays things out as a column: import androidx.compose.runtime.*
import com.varabyte.kobweb.compose.css.*
import com.varabyte.kobweb.compose.dom.ElementRefScope
import com.varabyte.kobweb.compose.foundation.layout.Column
import com.varabyte.kobweb.compose.foundation.layout.ColumnScope
import com.varabyte.kobweb.compose.foundation.layout.Row
import com.varabyte.kobweb.compose.foundation.layout.RowScope
import com.varabyte.kobweb.compose.foundation.layout.Spacer
import com.varabyte.kobweb.compose.ui.Modifier
import com.varabyte.kobweb.compose.ui.modifiers.*
import com.varabyte.kobweb.silk.components.overlay.Overlay
import com.varabyte.kobweb.silk.components.text.SpanText
import com.varabyte.kobweb.silk.style.CssStyle
import com.varabyte.kobweb.silk.style.base
import com.varabyte.kobweb.silk.style.selectors.children
import com.varabyte.kobweb.silk.style.toModifier
import com.varabyte.kobweb.silk.theme.colors.palette.background
import com.varabyte.kobweb.silk.theme.colors.palette.toPalette
import org.jetbrains.compose.web.css.*
import org.w3c.dom.HTMLElement
val ModalStyle = CssStyle.base {
Modifier
.minWidth(20.cssRem)
.maxWidth(30.cssRem)
.backgroundColor(colorMode.toPalette().background)
.margin(top = 6.vh)
.padding(1.cssRem)
.gap(1.cssRem)
.borderRadius(2.percent)
}
val ModalContentColumnStyle = CssStyle {
base {
Modifier
.fillMaxWidth()
.gap(1.cssRem)
.padding(1.cssRem)
.maxHeight(60.vh)
.overflow { y(Overflow.Auto) }
}
}
val ModalTitleStyle = CssStyle.base {
Modifier
.fillMaxWidth()
.margin(bottom = 1.cssRem)
.fontSize(2.cssRem)
.fontWeight(FontWeight.Bold)
}
val ModalButtonRowStyle = CssStyle {
base {
Modifier.fillMaxWidth().margin(top = 2.cssRem).gap(1.cssRem)
}
cssRule(" *") {
Modifier.flexGrow(1)
}
}
val FullWidthChildrenStyle = CssStyle {
cssRule(" > *") {
Modifier.fillMaxWidth()
}
}
@Composable
fun Modal(
overlayModifier: Modifier = Modifier,
dialogModifier: Modifier = Modifier,
ref: ElementRefScope<HTMLElement>? = null,
title: String? = null,
bottomRow: (@Composable RowScope.() -> Unit)? = null,
content: (@Composable ColumnScope.() -> Unit)? = null,
) {
Modal(
overlayModifier,
dialogModifier,
ref,
titleRow = @Suppress("NAME_SHADOWING") title?.let { title -> {
Spacer()
SpanText(title)
Spacer()
}},
bottomRow = bottomRow,
content = content
)
}
@Composable
fun Modal(
overlayModifier: Modifier = Modifier,
dialogModifier: Modifier = Modifier,
ref: ElementRefScope<HTMLElement>? = null,
titleRow: (@Composable RowScope.() -> Unit)? = null,
bottomRow: (@Composable RowScope.() -> Unit)? = null,
content: (@Composable ColumnScope.() -> Unit)? = null,
) {
Overlay(overlayModifier) {
Column(ModalStyle.toModifier().then(dialogModifier), ref = ref) {
if (titleRow != null) {
Row(ModalTitleStyle.toModifier()) { titleRow() }
}
content?.let { content ->
Column(listOf(ModalContentColumnStyle, FullWidthChildrenStyle).toModifier()) {
content()
}
}
if (bottomRow != null) {
Row(ModalButtonRowStyle.toModifier()) {
bottomRow()
}
}
}
}
}And using it looks like this: var isModalOpen by remember { mutableStateOf(false) }
Button(onClick = { isModalOpen = true }) {
Text("Open modal")
}
if (isModalOpen) {
Modal(
titleRow = {
Text("Title")
},
bottomRow = {
Button(onClick = { isModalOpen = false }) {
Text("Close")
}
}
) {
SpanText("Message #1")
SpanText("Message #2")
}
}I did update the code so it can work with Kobweb 0.18.0, but otherwise I haven't used this code in over a year so no promises that it won't need further tweaking. |
Beta Was this translation helpful? Give feedback.
-
|
thanks to @bitspittle answer .i have manage to make some pretty component out of it . i created a Alert Dialog , Full Screen Dialog , and notification alert (Toast alike). here is the code if anyone needed . its reusable . and has example at the end . just call DialogExamples() and see. import androidx.compose.runtime.*
import androidx.compose.web.events.SyntheticMouseEvent
import com.varabyte.kobweb.compose.css.BoxShadow
import com.varabyte.kobweb.compose.css.Cursor
import com.varabyte.kobweb.compose.css.FontWeight
import com.varabyte.kobweb.compose.css.PointerEvents
import com.varabyte.kobweb.compose.foundation.layout.Column
import com.varabyte.kobweb.compose.foundation.layout.ColumnScope
import com.varabyte.kobweb.compose.foundation.layout.Row
import com.varabyte.kobweb.compose.ui.Modifier
import com.varabyte.kobweb.compose.ui.modifiers.*
import com.varabyte.kobweb.compose.ui.toAttrs
import com.varabyte.kobweb.silk.components.forms.Button
import com.varabyte.kobweb.silk.components.icons.CloseIcon
import com.varabyte.kobweb.silk.components.overlay.Overlay
import com.varabyte.kobweb.silk.components.text.SpanText
import com.varabyte.kobweb.silk.style.CssStyle
import com.varabyte.kobweb.silk.style.base
import com.varabyte.kobweb.silk.style.toModifier
import com.varabyte.kobweb.silk.theme.colors.ColorMode
import com.varabyte.kobweb.silk.theme.colors.palette.background
import com.varabyte.kobweb.silk.theme.colors.palette.color
import com.varabyte.kobweb.silk.theme.colors.palette.toPalette
import kotlinx.coroutines.delay
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.js.Date
// ===== ALERT DIALOG STYLES =====
val AlertDialogStyle = CssStyle.base {
Modifier
.minWidth(20.cssRem)
.maxWidth(30.cssRem)
.backgroundColor(colorMode.toPalette().background)
.margin(top = 10.vh)
.padding(1.5.cssRem)
.gap(1.cssRem)
.borderRadius(8.px)
.boxShadow(BoxShadow.of(0.px, 4.px, 20.px, color = rgba(0, 0, 0, 0.15)))
}
val AlertTitleStyle = CssStyle.base {
Modifier
.fillMaxWidth()
.fontSize(1.25.cssRem)
.fontWeight(FontWeight.Bold)
.color(colorMode.toPalette().color)
}
val AlertContentStyle = CssStyle.base {
Modifier
.fillMaxWidth()
.fontSize(0.875.cssRem)
.lineHeight(1.5)
.color(colorMode.toPalette().color)
}
val AlertButtonRowStyle = CssStyle.base {
Modifier
.fillMaxWidth()
.gap(0.75.cssRem)
.justifyContent(org.jetbrains.compose.web.css.JustifyContent.Center)
.margin(top = 1.5.cssRem)
}
// ===== NOTIFICATION STYLES =====
enum class NotificationPosition {
TOP_START, TOP_END, BOTTOM_START, BOTTOM_END
}
enum class NotificationType {
SUCCESS, ERROR, WARNING, INFO
}
val NotificationContainerStyle = CssStyle.base {
Modifier
.position(Position.Fixed)
.zIndex(9999)
.padding(1.cssRem)
.pointerEvents(PointerEvents.None)
}
val NotificationStyle = CssStyle.base {
Modifier
.minWidth(20.cssRem)
.maxWidth(25.cssRem)
.padding(1.cssRem)
.borderRadius(8.px)
.boxShadow(BoxShadow.of(0.px, 4.px, 12.px, color = rgba(0, 0, 0, 0.15)))
.pointerEvents(PointerEvents.Auto)
.cursor(Cursor.Pointer)
.transition {
property("transform", "0.3s")
}
.transform { translateY(0.px) }
}
val NotificationTitleStyle = CssStyle.base {
Modifier
.fontWeight(FontWeight.SemiBold)
.fontSize(0.875.cssRem)
.margin(bottom = 0.25.cssRem)
}
val NotificationMessageStyle = CssStyle.base {
Modifier
.fontSize(0.75.cssRem)
.lineHeight(1.4)
.opacity(0.8)
}
// ===== FULL SCREEN DIALOG STYLES =====
val FullScreenDialogStyle = CssStyle.base {
Modifier
.fillMaxSize()
.backgroundColor(colorMode.toPalette().background)
.padding(0.px)
.borderRadius(0.px)
.position(Position.Fixed)
.top(0.px)
.left(0.px)
.zIndex(9998)
}
// ===== ALERT DIALOG COMPONENT =====
@Composable
fun AlertDialog(
title: String,
message: String,
confirmText: String = "OK",
dismissText: String? = null,
onConfirm: (SyntheticMouseEvent?) -> Unit,
onDismiss: (SyntheticMouseEvent?) -> Unit = {},
dialogModifier: Modifier = Modifier,
) {
Overlay(
Modifier
.zIndex(9999999f)
.onClick { if (dismissText != null) onDismiss(null) }
) {
Column(
AlertDialogStyle.toModifier().then(dialogModifier)
.onClick { it.stopPropagation() }
) {
// Title
SpanText(title, AlertTitleStyle.toModifier())
// Message
SpanText(message, AlertContentStyle.toModifier())
// Buttons
Row(AlertButtonRowStyle.toModifier()) {
if (dismissText != null) {
Button(
onClick = onDismiss,
modifier = Modifier.margin(right = 0.5.cssRem)
) {
SpanText(dismissText)
}
}
Button(onClick = onConfirm) {
SpanText(confirmText)
}
}
}
}
}
// ===== NOTIFICATION COMPONENT =====
@Composable
fun NotificationContainer(
position: NotificationPosition,
children: @Composable () -> Unit
) {
val positionModifier = when (position) {
NotificationPosition.TOP_START -> Modifier.top(0.px).left(0.px)
NotificationPosition.TOP_END -> Modifier.top(0.px).right(0.px)
NotificationPosition.BOTTOM_START -> Modifier.bottom(0.px).left(0.px)
NotificationPosition.BOTTOM_END -> Modifier.bottom(0.px).right(0.px)
}
Div(
NotificationContainerStyle.toModifier()
.zIndex(9999999f)
.then(positionModifier)
.toAttrs()
) {
children()
}
}
@Composable
fun Notification(
title: String,
message: String,
type: NotificationType = NotificationType.INFO,
autoHideDuration: Int = 5000, // milliseconds
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
var isVisible by remember { mutableStateOf(true) }
val typeModifier = when (type) {
NotificationType.SUCCESS -> Modifier.backgroundColor(Color.green).color(Color.white)
NotificationType.ERROR -> Modifier.backgroundColor(Color.red).color(Color.white)
NotificationType.WARNING -> Modifier.backgroundColor(Color.orange).color(Color.white)
NotificationType.INFO -> Modifier.backgroundColor(Color.blue).color(Color.white)
}
LaunchedEffect(Unit) {
if (autoHideDuration > 0) {
delay(autoHideDuration.toLong())
isVisible = false
onDismiss()
}
}
if (isVisible) {
Column(
NotificationStyle.toModifier()
.zIndex(9999999f)
.then(typeModifier)
.then(modifier)
.onClick { onDismiss() }
) {
SpanText(title, NotificationTitleStyle.toModifier())
SpanText(message, NotificationMessageStyle.toModifier())
}
}
}
// ===== FULL SCREEN DIALOG COMPONENT =====
@Composable
fun FullScreenDialog(
onDismiss: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Overlay(
Modifier
.zIndex(9999999f)
.width(100.vw)
.height(100.vh)
.backgroundColor(rgba(0, 0, 0, 0))
) {
IconButton(onClick = onDismiss) {
CloseIcon(
Modifier
.zIndex(9999999f)
)
}
Column(
FullScreenDialogStyle.toModifier()
) {
content()
}
}
}
// ===== USAGE EXAMPLES =====
@Composable
fun DialogExamples() {
val colorMode = ColorMode.current
var showAlert by remember { mutableStateOf(false) }
var showFullScreen by remember { mutableStateOf(false) }
var notifications by remember { mutableStateOf(listOf<NotificationData>()) }
// Main content
Column(Modifier.padding(2.cssRem).gap(1.cssRem)) {
Button(onClick = { showAlert = true }) {
SpanText("Show Alert Dialog")
}
Button(onClick = { showFullScreen = true }) {
SpanText("Show Full Screen Dialog")
}
Button(onClick = {
notifications = notifications + NotificationData(
id = Date.now().toString(),
title = "Success!",
message = "Operation completed successfully",
type = NotificationType.SUCCESS
)
console.log(notifications)
}) {
SpanText("Show Success Notification")
}
Button(onClick = {
notifications = notifications + NotificationData(
id = Date.now().toString(),
title = "Error!",
message = "Something went wrong",
type = NotificationType.ERROR
)
console.log(notifications)
}) {
SpanText("Show Error Notification")
}
}
// Alert Dialog
if (showAlert) {
AlertDialog(
title = "Confirmation",
message = "Are you sure you want to proceed with this action?",
confirmText = "Yes",
dismissText = "Cancel",
onConfirm = {
showAlert = false
// Handle confirm action
},
onDismiss = { showAlert = false }
)
}
// Full Screen Dialog
if (showFullScreen) {
FullScreenDialog(
onDismiss = { showFullScreen = false }
) {
// Header
Row(
Modifier
.fillMaxWidth()
.padding(1.cssRem)
.backgroundColor(colorMode.toPalette().background)
.justifyContent(JustifyContent.SpaceBetween)
.alignItems(AlignItems.Center)
) {
SpanText("Full Screen Dialog", Modifier.fontSize(1.25.cssRem).fontWeight(FontWeight.Bold))
Button(onClick = { showFullScreen = false }) {
SpanText("Close")
}
}
// Content
Column(
Modifier
.flex(1)
.padding(2.cssRem)
.gap(1.cssRem)
) {
SpanText("This is a full screen dialog content area.")
SpanText("You can put any content here.")
Button(onClick = {
// Handle some action
showFullScreen = false
}) {
SpanText("Save and Close")
}
}
}
}
// Notifications
NotificationContainer(NotificationPosition.TOP_END) {
notifications.forEach { notification ->
Notification(
title = notification.title,
message = notification.message,
type = notification.type,
autoHideDuration = 5000,
onDismiss = {
notifications = notifications.filter { it.id != notification.id }
},
modifier = Modifier.margin(bottom = 0.5.cssRem)
)
}
}
}
// Helper data class for notifications
data class NotificationData(
val id: String,
val title: String,
val message: String,
val type: NotificationType
) |
Beta Was this translation helpful? Give feedback.
Silk does not currently have a modal widget (one day, we plan to!), but here's something I implemented myself in an old project. Feel free to check it out and modify it as needed for your case. (Silk does have an
Overlaywidget that creates a full screen transparent gray foundation which we use here)Here, my modal class is designed to allow a row of title elements that appear at the top, a row at the bottom (useful for buttons), and then the content area, which lays things out as a column: