Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabIndicatorScope
Expand All @@ -30,6 +31,7 @@ fun PrezelTabs(
selectedTabIndex = pagerState.currentPage,
modifier = modifier.fillMaxWidth(),
containerColor = Color.Transparent,
divider = { HorizontalDivider(thickness = PrezelTheme.stroke.V2) },
indicator = { PrezelTabIndicator(selectedTabIndex = pagerState.currentPage) },
) {
tabs.forEachIndexed { index, label ->
Expand All @@ -51,7 +53,7 @@ private fun TabIndicatorScope.PrezelTabIndicator(
Spacer(
modifier = modifier
.fillMaxWidth()
.height(2.dp)
.height(PrezelTheme.stroke.V2)
.tabIndicatorOffset(selectedTabIndex = selectedTabIndex)
.background(PrezelTheme.colors.solidBlack),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.team.prezel.core.designsystem.component.textfield

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -10,23 +12,35 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand All @@ -37,6 +51,7 @@ import com.team.prezel.core.designsystem.preview.BasicPreview
import com.team.prezel.core.designsystem.preview.PreviewColumn
import com.team.prezel.core.designsystem.preview.PreviewSurface
import com.team.prezel.core.designsystem.theme.PrezelTheme
import kotlin.math.min

@Composable
fun PrezelTextArea(
Expand All @@ -51,33 +66,38 @@ fun PrezelTextArea(
showCount: Boolean = false,
minHeight: Dp = 72.dp,
fillContainerHeight: Boolean = false,
scrollState: ScrollState = rememberScrollState(),
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
require(maxLength >= 0) { "maxLength는 0 이상이어야 합니다." }

var focused by remember { mutableStateOf(false) }
var textFieldValue by remember { mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length))) }
val textFieldState = remember { TextFieldState(initialText = value) }
val currentValue by rememberUpdatedState(value)
val currentOnValueChange by rememberUpdatedState(onValueChange)

LaunchedEffect(value) {
if (value != textFieldValue.text) {
textFieldValue = TextFieldValue(text = value, selection = TextRange(value.length))
if (value != textFieldState.text.toString()) {
textFieldState.setTextAndPlaceCursorAtEnd(value)
}
}

LaunchedEffect(textFieldState) {
snapshotFlow { textFieldState.text.toString() }
.collect { newValue ->
if (newValue != currentValue) currentOnValueChange(newValue)
}
}

val style = rememberPrezelTextFieldState(
value = textFieldValue.text,
value = textFieldState.text.toString(),
enabled = enabled,
focused = focused,
).let { state -> PrezelTextFieldStyle(state = state, status = status) }

PrezelTextArea(
value = textFieldValue,
onValueChange = { newValue ->
val applied = applyPrezelTextInputPolicy(currentValue = textFieldValue, newValue = newValue, maxLength = maxLength)
if (applied != textFieldValue) {
textFieldValue = applied
if (applied.text != value) onValueChange(applied.text)
}
},
textFieldState = textFieldState,
placeholder = placeholder,
style = style,
maxLength = maxLength,
Expand All @@ -89,15 +109,15 @@ fun PrezelTextArea(
showCount = showCount,
minHeight = minHeight,
fillContainerHeight = fillContainerHeight,
scrollState = scrollState,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}

@Composable
private fun PrezelTextArea(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
textFieldState: TextFieldState,
placeholder: String,
maxLength: Int,
style: PrezelTextFieldStyle,
Expand All @@ -106,21 +126,25 @@ private fun PrezelTextArea(
label: String?,
enabled: Boolean,
showCount: Boolean,
modifier: Modifier = Modifier,
minHeight: Dp = 72.dp,
fillContainerHeight: Boolean,
scrollState: ScrollState,
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
modifier: Modifier = Modifier,
) {
val keyboardActionHandler = remember(keyboardActions, keyboardOptions.imeAction) {
keyboardActions.toKeyboardActionHandler(keyboardOptions.imeAction)
}

Column(modifier = modifier.fillMaxWidth()) {
label?.let {
PrezelTextFieldLabel(label = it)
Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8))
}

BasicTextField(
value = value,
onValueChange = onValueChange,
state = textFieldState,
enabled = enabled,
modifier = Modifier
.fillMaxWidth()
Expand All @@ -134,20 +158,26 @@ private fun PrezelTextArea(
textStyle = PrezelTheme.typography.body2Regular.copy(color = style.textColor()),
cursorBrush = SolidColor(PrezelTheme.colors.interactiveRegular),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = { innerTextField ->
onKeyboardAction = keyboardActionHandler,
inputTransformation = InputTransformation.maxLength(maxLength),
scrollState = scrollState,
decorator = { innerTextField ->
PrezelTextAreaDecorationBox(
innerTextField = innerTextField,
showPlaceholder = !focused && value.text.isEmpty(),
showPlaceholder = !focused && textFieldState.text.isEmpty(),
placeholder = placeholder,
state = style,
showCounter = showCount,
scrollState = scrollState,
counter = {
if (showCount) {
Counter(currentLength = value.text.length, maxLength = maxLength, state = style)
Counter(currentLength = textFieldState.text.length, maxLength = maxLength, state = style)
}
},
modifier = if (fillContainerHeight) Modifier.fillMaxHeight() else Modifier.heightIn(min = minHeight),
modifier = when {
fillContainerHeight -> Modifier.fillMaxHeight()
else -> Modifier.heightIn(min = minHeight)
},
fillContainerHeight = fillContainerHeight,
)
},
Expand Down Expand Up @@ -184,6 +214,7 @@ private fun PrezelTextAreaDecorationBox(
placeholder: String,
state: PrezelTextFieldStyle,
showCounter: Boolean,
scrollState: ScrollState,
modifier: Modifier = Modifier,
fillContainerHeight: Boolean = false,
) {
Expand All @@ -194,29 +225,102 @@ private fun PrezelTextAreaDecorationBox(
border = state.borderStroke(),
contentColor = state.textColor(),
) {
val showScrollbar = scrollState.maxValue > 0
val endPadding = if (showScrollbar) PrezelTheme.spacing.V8 else PrezelTheme.spacing.V12
val scrollbarContentPadding = if (showScrollbar) PrezelTheme.spacing.V8 else 0.dp

Box(
modifier = Modifier
.then(if (fillContainerHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth())
.padding(PrezelTheme.spacing.V12),
.padding(
start = PrezelTheme.spacing.V12,
top = PrezelTheme.spacing.V12,
end = endPadding,
bottom = PrezelTheme.spacing.V12,
),
) {
Box(
modifier = Modifier
.then(if (fillContainerHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth())
.padding(bottom = if (showCounter) PrezelTheme.spacing.V24 else 0.dp),
.padding(
end = scrollbarContentPadding,
bottom = if (showCounter) PrezelTheme.spacing.V24 else 0.dp,
),
) {
innerTextField()
if (showPlaceholder) {
PrezelTextFieldPlaceholder(placeholder = placeholder)
}
}

Box(modifier = Modifier.align(Alignment.BottomEnd)) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = scrollbarContentPadding),
) {
counter()
}

if (showScrollbar) {
PrezelTextAreaScrollbar(
scrollState = scrollState,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight()
.width(PrezelTheme.spacing.V4),
)
}
}
}
}

@Composable
private fun PrezelTextAreaScrollbar(
scrollState: ScrollState,
modifier: Modifier = Modifier,
) {
val color = PrezelTheme.colors.borderLarge
val minThumbHeight = PrezelTheme.spacing.V24

Canvas(modifier = modifier) {
if (scrollState.maxValue <= 0 || scrollState.viewportSize <= 0) return@Canvas

val viewportHeight = scrollState.viewportSize.toFloat()
val contentHeight = viewportHeight + scrollState.maxValue
val minThumbHeightPx = min(minThumbHeight.toPx(), size.height)
val thumbHeight = (size.height * viewportHeight / contentHeight)
.coerceIn(minThumbHeightPx, size.height)
val thumbOffset = (size.height - thumbHeight) * scrollState.value / scrollState.maxValue

drawRoundRect(
color = color,
topLeft = Offset(x = 0f, y = thumbOffset),
size = Size(width = size.width, height = thumbHeight),
cornerRadius = CornerRadius(size.width / 2f),
)
}
}

private fun KeyboardActions.toKeyboardActionHandler(imeAction: ImeAction): KeyboardActionHandler? {
val action = when (imeAction) {
ImeAction.Done -> onDone
ImeAction.Go -> onGo
ImeAction.Next -> onNext
ImeAction.Previous -> onPrevious
ImeAction.Search -> onSearch
ImeAction.Send -> onSend
else -> null
} ?: return null

return KeyboardActionHandler { performDefaultAction ->
action.invoke(
object : KeyboardActionScope {
override fun defaultKeyboardAction(imeAction: ImeAction) = performDefaultAction()
},
)
}
}

@BasicPreview
@Composable
private fun PrezelTextAreaDefaultStatePreview() {
Expand Down Expand Up @@ -310,6 +414,28 @@ private fun PrezelTextAreaTypedStatePreview() {
}
}

@BasicPreview
@Composable
private fun PrezelTextAreaScrollbarPreview() {
PreviewTextAreaState(title = "Type - With Scrollbar") {
PrezelTextAreaPreviewItem(
label = "Label",
value = "Lorem ipsum dolor sit amet consectetur. Consequat quis viverra nulla in aliquam sed " +
"scelerisque odio gravida. At urna congue vulputate facilisis id et viverra pellentesque " +
"tempus. Blandit et faucibus iaculis dictum pharetra. Magna elit lacus nullam facilisi amet " +
"urna pulvinar.",
state = PrezelTextFieldStyle(
state = PrezelTextFieldState.TYPED,
status = PrezelTextFieldStatus.Default("Helper"),
),
modifier = Modifier.height(200.dp),
showCount = false,
fillContainerHeight = true,
maxLength = 500,
)
}
}

@Composable
private fun PreviewTextAreaState(
title: String,
Expand Down Expand Up @@ -338,20 +464,26 @@ private fun PrezelTextAreaPreviewItem(
modifier: Modifier = Modifier,
enabled: Boolean = true,
focused: Boolean = false,
showCount: Boolean = true,
fillContainerHeight: Boolean = false,
maxLength: Int = 100,
) {
val textFieldState = remember { TextFieldState(initialText = value) }

PrezelTextArea(
value = TextFieldValue(text = value, selection = TextRange(value.length)),
onValueChange = {},
textFieldState = textFieldState,
placeholder = "Placeholder",
label = label,
style = state,
maxLength = 100,
maxLength = maxLength,
focused = focused,
modifier = modifier,
onFocusChange = {},
enabled = enabled,
showCount = true,
fillContainerHeight = false,
showCount = showCount,
minHeight = 72.dp,
fillContainerHeight = fillContainerHeight,
scrollState = rememberScrollState(),
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions.Default,
)
Expand Down
Loading