-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#8] 필드 공통 검포넌트 구현 #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e3d4191
bcf7af1
8d217df
bc232a8
b79d25e
cd74801
0904355
f8fe6be
d13dfa9
3869aa3
48ba8af
898ff9d
822c0d5
00106e8
853dd67
c32c57f
eb17a11
e20ccf7
ee16ef6
54f1cb0
77bc749
9c45c0b
75a928c
d3779b9
09edfa6
df427b0
c6c4f33
0d641c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.poti.android.core.designsystem.component.field | ||
|
|
||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import com.poti.android.core.designsystem.theme.PotiTheme | ||
|
|
||
| // TODO: [도연] Display>ErrorMessage 병합 시 삭제 | ||
| @Composable | ||
| fun FieldErrorMessage( | ||
| error: String, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| if (error.isNotEmpty()) { | ||
| Row( | ||
| modifier = modifier, | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| Text( | ||
| text = error, | ||
| color = PotiTheme.colors.sementicRed, | ||
| style = PotiTheme.typography.body14m, | ||
| ) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.poti.android.core.designsystem.component.field | ||
|
|
||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import com.poti.android.core.designsystem.theme.PotiTheme | ||
|
|
||
| @Composable | ||
| internal fun FieldLabel( | ||
| label: String, | ||
| ) { | ||
| if (label.isNotEmpty()) { | ||
| Text( | ||
| text = label, | ||
| color = PotiTheme.colors.black, | ||
| style = PotiTheme.typography.body14sb, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package com.poti.android.core.designsystem.component.field | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.foundation.text.BasicTextField | ||
| import androidx.compose.foundation.text.KeyboardActions | ||
| import androidx.compose.foundation.text.KeyboardOptions | ||
| import androidx.compose.material3.Text | ||
| 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 androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.draw.clip | ||
| import androidx.compose.ui.focus.FocusRequester | ||
| import androidx.compose.ui.focus.focusRequester | ||
| import androidx.compose.ui.focus.onFocusChanged | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.text.input.ImeAction | ||
| import androidx.compose.ui.text.input.KeyboardType | ||
| import androidx.compose.ui.text.style.TextOverflow | ||
| import androidx.compose.ui.unit.dp | ||
| import com.poti.android.core.common.extension.roundedBackgroundWithBorder | ||
| import com.poti.android.core.designsystem.theme.PotiTheme | ||
|
|
||
| @Composable | ||
| internal fun PotiBasicField( | ||
| value: String, | ||
| onValueChanged: (String) -> Unit, | ||
| placeholder: String, | ||
| borderColor: Color, | ||
| backgroundColor: Color, | ||
| modifier: Modifier = Modifier, | ||
| keyboardType: KeyboardType = KeyboardType.Text, | ||
| imeAction: ImeAction = ImeAction.Done, | ||
| onDoneAction: () -> Unit = {}, | ||
| onNextAction: () -> Unit = {}, | ||
| onSearchAction: () -> Unit = {}, | ||
| onFocusChanged: (Boolean) -> Unit = {}, | ||
| focusRequester: FocusRequester? = null, | ||
| singleLine: Boolean = true, | ||
| trailingIcon: @Composable () -> Unit = {}, | ||
| enabled: Boolean = true, | ||
| ) { | ||
| var isFocused by remember { mutableStateOf(false) } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p1: 상태를 컴퍼저블 내부에서 가지고 있으면 안될거 같아요.
이렇게 하면 부모 컴포저블에서 상태관리도 가능하면서, 기존 기능 유지할 수 있을거 같아요! 만약 적용한다면, 다른 Field 도 동일할 것 같습니다!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 움 왜 안되는지 이유도 함께 적어주심 감사드립니다!
BasicField는 부모 컴포저블이 관리하는 컴포넌트이기 보다는, 최소한의 로직과 기본 레이아웃을 보장하는 컴포넌트로서 이를 래핑해 유연하게 사용하는 목적으로 설계했어요. |
||
| val requester = remember { | ||
| focusRequester ?: FocusRequester() | ||
| } | ||
|
|
||
| BasicTextField( | ||
| value = value, | ||
| onValueChange = onValueChanged, | ||
| modifier = modifier | ||
| .clip(RoundedCornerShape(8.dp)) | ||
| .roundedBackgroundWithBorder( | ||
| cornerRadius = 8.dp, | ||
| backgroundColor = backgroundColor, | ||
| borderColor = borderColor, | ||
| borderWidth = 1.dp, | ||
| ) | ||
| .focusRequester(requester) | ||
| .onFocusChanged { focusState -> | ||
| isFocused = focusState.isFocused | ||
| onFocusChanged(focusState.isFocused) | ||
| }, | ||
| singleLine = singleLine, | ||
| keyboardOptions = KeyboardOptions( | ||
| keyboardType = keyboardType, | ||
| imeAction = imeAction, | ||
| ), | ||
| keyboardActions = KeyboardActions( | ||
| onDone = { onDoneAction() }, | ||
| onSearch = { onSearchAction() }, | ||
| onNext = { onNextAction() }, | ||
| ), | ||
| enabled = enabled, | ||
| textStyle = PotiTheme.typography.body16m.copy( | ||
| color = PotiTheme.colors.black, | ||
| ), | ||
| decorationBox = { innerTextField -> | ||
| Row( | ||
| modifier = Modifier | ||
| .padding(horizontal = 16.dp, vertical = 14.dp), | ||
| verticalAlignment = if (singleLine) Alignment.CenterVertically else Alignment.Top, | ||
| ) { | ||
| Box( | ||
| modifier = Modifier | ||
| .weight(1f), | ||
| contentAlignment = Alignment.CenterStart, | ||
| ) { | ||
| innerTextField() | ||
|
|
||
| if (value.isEmpty()) { | ||
| Text( | ||
| text = placeholder, | ||
| color = PotiTheme.colors.gray700, | ||
| style = PotiTheme.typography.body16m, | ||
| ) | ||
| } | ||
|
|
||
| if (singleLine && !isFocused && value.isNotEmpty()) { | ||
| Text( | ||
| text = value, | ||
| modifier = Modifier | ||
| .background(backgroundColor), | ||
| color = PotiTheme.colors.black, | ||
| style = PotiTheme.typography.body16m, | ||
| maxLines = 1, | ||
| overflow = TextOverflow.Ellipsis, | ||
| ) | ||
| } | ||
| } | ||
| trailingIcon() | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| enum class FieldStatus { | ||
| DEFAULT, | ||
| FOCUS, | ||
| ERROR, | ||
| } | ||
|
|
||
| val FieldStatus.borderColor: Color | ||
| @Composable get() = when (this) { | ||
| FieldStatus.DEFAULT -> PotiTheme.colors.gray300 | ||
| FieldStatus.FOCUS -> PotiTheme.colors.gray700 | ||
| FieldStatus.ERROR -> PotiTheme.colors.sementicRed | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| package com.poti.android.core.designsystem.component.field | ||
|
|
||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.heightIn | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.material3.Text | ||
| 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 androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.focus.FocusDirection | ||
| import androidx.compose.ui.focus.FocusRequester | ||
| import androidx.compose.ui.platform.LocalFocusManager | ||
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | ||
| import androidx.compose.ui.text.input.ImeAction | ||
| import androidx.compose.ui.text.input.KeyboardType | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.poti.android.core.designsystem.theme.PotiTheme | ||
|
|
||
| /** | ||
| * 우측에 글자 수 카운트가 표시되는 텍스트 필드입니다. | ||
| * 최대 한 줄로 노출되며, 길게 작성된 경우 포커스 아웃 시 말줄임 처리 됩니다. | ||
| * maxLength는 UI 표시 용이며, 내부에 글자 수를 제어하는 로직은 없으므로 글자 수 제한은 외부에서 제어해 value로 넣어줍니다. | ||
| * | ||
| * @param value 필드 입력값입니다. | ||
| * @param onValueChanged 필드에 입력된 값을 전달합니다. | ||
| * @param placeholder 입력값이 없을 때 표시됩니다. | ||
| * @param maxLength 최대 글자 수로 표시됩니다. | ||
| * @param modifier | ||
| * @param keyboardType 키보드 입력 타입으로, 기본값은 Text입니다. | ||
| * @param label 필드 상단에 표시됩니다. | ||
| * @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다. | ||
| * @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next로 변경 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. | ||
| * @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다. | ||
| * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. | ||
| * | ||
| * @author 도연 | ||
| * @sample PotiCountFieldWithLabelPreview | ||
| */ | ||
| @Composable | ||
| fun PotiCountField( | ||
| value: String, | ||
| onValueChanged: (String) -> Unit, | ||
| placeholder: String, | ||
| maxLength: Int, | ||
| modifier: Modifier = Modifier, | ||
| keyboardType: KeyboardType = KeyboardType.Text, | ||
| label: String = "", | ||
| error: String = "", | ||
| imeAction: ImeAction = ImeAction.Done, | ||
| focusRequester: FocusRequester? = null, | ||
| enabled: Boolean = true, | ||
| ) { | ||
| var isFocused by remember { mutableStateOf(false) } | ||
|
|
||
| val focusManager = LocalFocusManager.current | ||
| val keyboardController = LocalSoftwareKeyboardController.current | ||
|
|
||
| val status = when { | ||
| error.isNotEmpty() -> FieldStatus.ERROR | ||
| isFocused -> FieldStatus.FOCUS | ||
| else -> FieldStatus.DEFAULT | ||
| } | ||
|
|
||
| Column( | ||
| modifier = modifier, | ||
| verticalArrangement = Arrangement.spacedBy(8.dp), | ||
| ) { | ||
| FieldLabel(label) | ||
|
|
||
| PotiBasicField( | ||
| value = value, | ||
| onValueChanged = onValueChanged, | ||
| placeholder = placeholder, | ||
| borderColor = status.borderColor, | ||
| backgroundColor = PotiTheme.colors.white, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .heightIn(52.dp), | ||
| keyboardType = keyboardType, | ||
| imeAction = imeAction, | ||
| onDoneAction = { | ||
| keyboardController?.hide() | ||
| focusManager.clearFocus() | ||
| }, | ||
| onNextAction = { | ||
| focusManager.moveFocus(FocusDirection.Down) | ||
| }, | ||
| onFocusChanged = { isFocused = it }, | ||
| focusRequester = focusRequester, | ||
| singleLine = true, | ||
| trailingIcon = { | ||
| Text( | ||
| text = "${value.length}/$maxLength", | ||
| modifier = Modifier.padding(bottom = 3.dp), | ||
| color = PotiTheme.colors.gray700, | ||
| style = PotiTheme.typography.body14m, | ||
| ) | ||
| }, | ||
| enabled = enabled, | ||
| ) | ||
|
|
||
| // TODO: [도연] Display>ErrorMessage으로 대체 | ||
| FieldErrorMessage(error) | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| private fun PotiCountFieldWithErrorPreview() { | ||
| var text by remember { mutableStateOf("") } | ||
|
|
||
| PotiTheme { | ||
| PotiCountField( | ||
| value = text, | ||
| onValueChanged = { text = it }, | ||
| placeholder = "플레이스홀더", | ||
| error = "에러 메시지", | ||
| maxLength = 10, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| private fun PotiCountFieldWithLabelPreview() { | ||
| var text by remember { mutableStateOf("") } | ||
|
|
||
| PotiTheme { | ||
| PotiCountField( | ||
| value = text, | ||
| onValueChanged = { text = it }, | ||
| placeholder = "플레이스홀더", | ||
| label = "라벨", | ||
| maxLength = 10, | ||
| ) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.