Skip to content

Commit 614af57

Browse files
authored
[Feat/#8] 필드 공통 검포넌트 구현 (#14)
* [Feat/#8] 베이직 필드 * [Feat/#8] 롱/쇼트 텍스트 필드 * [Feat/#8] 카운트 필드 * [Feat/#8] 커스텀 드롭다운메뉴 * [Feat/#8] 드롭다운 필드 * [Fix/#8] 베이직 필드 포커스아웃 시 말줄임 텍스트 표시 로직 수정 as-is:innerTextField()가 말줄임 텍스트에 대체되어, 재포커스 시 커서 위치가 맨 앞으로 초기화됨 to-be:innerTextField()는 항시 유지하고 말줄임 텍스트 컴포저블을 스택으로 쌓아 커서 위치 유지 * [Feat/#8] 서치 필드 * [Chore/#8] kdoc 주석 추가 및 klint 적용 * [Fix/#8] 롱 텍스트 필드 키보드 액션 디폴트를 기본값으로 변경 * [Fix/#8] 하드코딩 제거 및 파라미터값 전달 * [Fix/#8] disabled인 메뉴 아이템 터치 이벤트 차단 * [Fix/#8] 드롭다운 open 상태에 따른 arrow 아이콘 수정 * [Feat/#8] 드롭다운 메뉴 최대 높이 기본값 및 타입 설정 * [Fix/#8] 서치 필드 포커스 보더 컬러 로직 수정 as-is : 메뉴 오픈 시 보더 컬러 변경 to-be : 필드 포커스 시 보더 컬러 변경 * [Feat/#8] 에러 케이스 및 라벨 추가 * [Chore/#8] klint 적용 * [Fix/#8] 불필요한 딜레이 제거 * [Chore/#8] 주석 수정 * [Refactor/#19] modifier 익스텐션 적용 * [Fix/#19] 오탈자 수정 * [Refactor/#19] 드롭다운/서치 필드 리팩토링 - 아이템 간 디바이더 대신 bottomBorder 적용 - 서치 필드 포커스 로직 제거 - 드롭다운 필드의 메뉴 최대 높이 아이템 높이에 따라 조정 - 메뉴 아이템 패딩 수정 및 최소 높이 설정, 리플 익스텐션 적용 - 드롭다운 메뉴 Column -> LazyColumn 변경 * [Refactor/#19] 필드 리팩토링 - FieldStatus 통해 borderColor 관리 - focusRequester 기본값 null로 하고, null인 경우에만 PotiBasicField 내에서 객체 생성 * [Refactor/#19] interactionSource 파라미터 추가 * [Chore/#19] klint 적용 * [Chore/#19] 오탈자 수정
1 parent fe89ddd commit 614af57

File tree

13 files changed

+1324
-1
lines changed

13 files changed

+1324
-1
lines changed

app/src/main/java/com/poti/android/core/common/extension/ModifierExt.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
1212
import androidx.compose.ui.composed
1313
import androidx.compose.ui.draw.clip
1414
import androidx.compose.ui.draw.drawBehind
15+
import androidx.compose.ui.geometry.Offset
1516
import androidx.compose.ui.geometry.Size
1617
import androidx.compose.ui.graphics.Color
1718
import androidx.compose.ui.graphics.Paint
@@ -136,3 +137,24 @@ fun Modifier.topRoundedBorder(
136137
style = Stroke(width = strokeWidthPx),
137138
)
138139
}
140+
141+
fun Modifier.bottomBorder(
142+
strokeWidth: Dp,
143+
color: Color,
144+
isVisible: Boolean = true,
145+
): Modifier {
146+
if (!isVisible) return this
147+
148+
return this.drawBehind {
149+
val strokeWidthPx = strokeWidth.toPx()
150+
val width = size.width
151+
val height = size.height - strokeWidthPx / 2
152+
153+
drawLine(
154+
color = color,
155+
start = Offset(x = 0f, y = height),
156+
end = Offset(x = width, y = height),
157+
strokeWidth = strokeWidthPx,
158+
)
159+
}
160+
}

app/src/main/java/com/poti/android/core/designsystem/component/field/DummyFieldComponent.kt

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.poti.android.core.designsystem.component.field
2+
3+
import androidx.compose.foundation.layout.Row
4+
import androidx.compose.material3.Text
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Alignment
7+
import androidx.compose.ui.Modifier
8+
import com.poti.android.core.designsystem.theme.PotiTheme
9+
10+
// TODO: [도연] Display>ErrorMessage 병합 시 삭제
11+
@Composable
12+
fun FieldErrorMessage(
13+
error: String,
14+
modifier: Modifier = Modifier,
15+
) {
16+
if (error.isNotEmpty()) {
17+
Row(
18+
modifier = modifier,
19+
verticalAlignment = Alignment.CenterVertically,
20+
) {
21+
Text(
22+
text = error,
23+
color = PotiTheme.colors.sementicRed,
24+
style = PotiTheme.typography.body14m,
25+
)
26+
}
27+
}
28+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.poti.android.core.designsystem.component.field
2+
3+
import androidx.compose.material3.Text
4+
import androidx.compose.runtime.Composable
5+
import com.poti.android.core.designsystem.theme.PotiTheme
6+
7+
@Composable
8+
internal fun FieldLabel(
9+
label: String,
10+
) {
11+
if (label.isNotEmpty()) {
12+
Text(
13+
text = label,
14+
color = PotiTheme.colors.black,
15+
style = PotiTheme.typography.body14sb,
16+
)
17+
}
18+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.poti.android.core.designsystem.component.field
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.shape.RoundedCornerShape
8+
import androidx.compose.foundation.text.BasicTextField
9+
import androidx.compose.foundation.text.KeyboardActions
10+
import androidx.compose.foundation.text.KeyboardOptions
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.mutableStateOf
15+
import androidx.compose.runtime.remember
16+
import androidx.compose.runtime.setValue
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.draw.clip
20+
import androidx.compose.ui.focus.FocusRequester
21+
import androidx.compose.ui.focus.focusRequester
22+
import androidx.compose.ui.focus.onFocusChanged
23+
import androidx.compose.ui.graphics.Color
24+
import androidx.compose.ui.text.input.ImeAction
25+
import androidx.compose.ui.text.input.KeyboardType
26+
import androidx.compose.ui.text.style.TextOverflow
27+
import androidx.compose.ui.unit.dp
28+
import com.poti.android.core.common.extension.roundedBackgroundWithBorder
29+
import com.poti.android.core.designsystem.theme.PotiTheme
30+
31+
@Composable
32+
internal fun PotiBasicField(
33+
value: String,
34+
onValueChanged: (String) -> Unit,
35+
placeholder: String,
36+
borderColor: Color,
37+
backgroundColor: Color,
38+
modifier: Modifier = Modifier,
39+
keyboardType: KeyboardType = KeyboardType.Text,
40+
imeAction: ImeAction = ImeAction.Done,
41+
onDoneAction: () -> Unit = {},
42+
onNextAction: () -> Unit = {},
43+
onSearchAction: () -> Unit = {},
44+
onFocusChanged: (Boolean) -> Unit = {},
45+
focusRequester: FocusRequester? = null,
46+
singleLine: Boolean = true,
47+
trailingIcon: @Composable () -> Unit = {},
48+
enabled: Boolean = true,
49+
) {
50+
var isFocused by remember { mutableStateOf(false) }
51+
val requester = remember {
52+
focusRequester ?: FocusRequester()
53+
}
54+
55+
BasicTextField(
56+
value = value,
57+
onValueChange = onValueChanged,
58+
modifier = modifier
59+
.clip(RoundedCornerShape(8.dp))
60+
.roundedBackgroundWithBorder(
61+
cornerRadius = 8.dp,
62+
backgroundColor = backgroundColor,
63+
borderColor = borderColor,
64+
borderWidth = 1.dp,
65+
)
66+
.focusRequester(requester)
67+
.onFocusChanged { focusState ->
68+
isFocused = focusState.isFocused
69+
onFocusChanged(focusState.isFocused)
70+
},
71+
singleLine = singleLine,
72+
keyboardOptions = KeyboardOptions(
73+
keyboardType = keyboardType,
74+
imeAction = imeAction,
75+
),
76+
keyboardActions = KeyboardActions(
77+
onDone = { onDoneAction() },
78+
onSearch = { onSearchAction() },
79+
onNext = { onNextAction() },
80+
),
81+
enabled = enabled,
82+
textStyle = PotiTheme.typography.body16m.copy(
83+
color = PotiTheme.colors.black,
84+
),
85+
decorationBox = { innerTextField ->
86+
Row(
87+
modifier = Modifier
88+
.padding(horizontal = 16.dp, vertical = 14.dp),
89+
verticalAlignment = if (singleLine) Alignment.CenterVertically else Alignment.Top,
90+
) {
91+
Box(
92+
modifier = Modifier
93+
.weight(1f),
94+
contentAlignment = Alignment.CenterStart,
95+
) {
96+
innerTextField()
97+
98+
if (value.isEmpty()) {
99+
Text(
100+
text = placeholder,
101+
color = PotiTheme.colors.gray700,
102+
style = PotiTheme.typography.body16m,
103+
)
104+
}
105+
106+
if (singleLine && !isFocused && value.isNotEmpty()) {
107+
Text(
108+
text = value,
109+
modifier = Modifier
110+
.background(backgroundColor),
111+
color = PotiTheme.colors.black,
112+
style = PotiTheme.typography.body16m,
113+
maxLines = 1,
114+
overflow = TextOverflow.Ellipsis,
115+
)
116+
}
117+
}
118+
trailingIcon()
119+
}
120+
},
121+
)
122+
}
123+
124+
enum class FieldStatus {
125+
DEFAULT,
126+
FOCUS,
127+
ERROR,
128+
}
129+
130+
val FieldStatus.borderColor: Color
131+
@Composable get() = when (this) {
132+
FieldStatus.DEFAULT -> PotiTheme.colors.gray300
133+
FieldStatus.FOCUS -> PotiTheme.colors.gray700
134+
FieldStatus.ERROR -> PotiTheme.colors.sementicRed
135+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.poti.android.core.designsystem.component.field
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.heightIn
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.setValue
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.focus.FocusDirection
16+
import androidx.compose.ui.focus.FocusRequester
17+
import androidx.compose.ui.platform.LocalFocusManager
18+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
19+
import androidx.compose.ui.text.input.ImeAction
20+
import androidx.compose.ui.text.input.KeyboardType
21+
import androidx.compose.ui.tooling.preview.Preview
22+
import androidx.compose.ui.unit.dp
23+
import com.poti.android.core.designsystem.theme.PotiTheme
24+
25+
/**
26+
* 우측에 글자 수 카운트가 표시되는 텍스트 필드입니다.
27+
* 최대 한 줄로 노출되며, 길게 작성된 경우 포커스 아웃 시 말줄임 처리 됩니다.
28+
* maxLength는 UI 표시 용이며, 내부에 글자 수를 제어하는 로직은 없으므로 글자 수 제한은 외부에서 제어해 value로 넣어줍니다.
29+
*
30+
* @param value 필드 입력값입니다.
31+
* @param onValueChanged 필드에 입력된 값을 전달합니다.
32+
* @param placeholder 입력값이 없을 때 표시됩니다.
33+
* @param maxLength 최대 글자 수로 표시됩니다.
34+
* @param modifier
35+
* @param keyboardType 키보드 입력 타입으로, 기본값은 Text입니다.
36+
* @param label 필드 상단에 표시됩니다.
37+
* @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다.
38+
* @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next로 변경 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다.
39+
* @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다.
40+
* @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다.
41+
*
42+
* @author 도연
43+
* @sample PotiCountFieldWithLabelPreview
44+
*/
45+
@Composable
46+
fun PotiCountField(
47+
value: String,
48+
onValueChanged: (String) -> Unit,
49+
placeholder: String,
50+
maxLength: Int,
51+
modifier: Modifier = Modifier,
52+
keyboardType: KeyboardType = KeyboardType.Text,
53+
label: String = "",
54+
error: String = "",
55+
imeAction: ImeAction = ImeAction.Done,
56+
focusRequester: FocusRequester? = null,
57+
enabled: Boolean = true,
58+
) {
59+
var isFocused by remember { mutableStateOf(false) }
60+
61+
val focusManager = LocalFocusManager.current
62+
val keyboardController = LocalSoftwareKeyboardController.current
63+
64+
val status = when {
65+
error.isNotEmpty() -> FieldStatus.ERROR
66+
isFocused -> FieldStatus.FOCUS
67+
else -> FieldStatus.DEFAULT
68+
}
69+
70+
Column(
71+
modifier = modifier,
72+
verticalArrangement = Arrangement.spacedBy(8.dp),
73+
) {
74+
FieldLabel(label)
75+
76+
PotiBasicField(
77+
value = value,
78+
onValueChanged = onValueChanged,
79+
placeholder = placeholder,
80+
borderColor = status.borderColor,
81+
backgroundColor = PotiTheme.colors.white,
82+
modifier = Modifier
83+
.fillMaxWidth()
84+
.heightIn(52.dp),
85+
keyboardType = keyboardType,
86+
imeAction = imeAction,
87+
onDoneAction = {
88+
keyboardController?.hide()
89+
focusManager.clearFocus()
90+
},
91+
onNextAction = {
92+
focusManager.moveFocus(FocusDirection.Down)
93+
},
94+
onFocusChanged = { isFocused = it },
95+
focusRequester = focusRequester,
96+
singleLine = true,
97+
trailingIcon = {
98+
Text(
99+
text = "${value.length}/$maxLength",
100+
modifier = Modifier.padding(bottom = 3.dp),
101+
color = PotiTheme.colors.gray700,
102+
style = PotiTheme.typography.body14m,
103+
)
104+
},
105+
enabled = enabled,
106+
)
107+
108+
// TODO: [도연] Display>ErrorMessage으로 대체
109+
FieldErrorMessage(error)
110+
}
111+
}
112+
113+
@Preview
114+
@Composable
115+
private fun PotiCountFieldWithErrorPreview() {
116+
var text by remember { mutableStateOf("") }
117+
118+
PotiTheme {
119+
PotiCountField(
120+
value = text,
121+
onValueChanged = { text = it },
122+
placeholder = "플레이스홀더",
123+
error = "에러 메시지",
124+
maxLength = 10,
125+
)
126+
}
127+
}
128+
129+
@Preview
130+
@Composable
131+
private fun PotiCountFieldWithLabelPreview() {
132+
var text by remember { mutableStateOf("") }
133+
134+
PotiTheme {
135+
PotiCountField(
136+
value = text,
137+
onValueChanged = { text = it },
138+
placeholder = "플레이스홀더",
139+
label = "라벨",
140+
maxLength = 10,
141+
)
142+
}
143+
}

0 commit comments

Comments
 (0)