forked from microsoft/fluentui-android
-
Notifications
You must be signed in to change notification settings - Fork 0
/
PeoplePicker.kt
286 lines (278 loc) · 13.9 KB
/
PeoplePicker.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package com.microsoft.fluentui.tokenized.peoplepicker
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.LayoutDirection
import com.microsoft.fluentui.icons.SearchBarIcons
import com.microsoft.fluentui.icons.searchbaricons.Dismisscircle
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
import com.microsoft.fluentui.theme.token.FluentIcon
import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
import com.microsoft.fluentui.theme.token.controlTokens.PeoplePickerInfo
import com.microsoft.fluentui.theme.token.controlTokens.PeoplePickerTokens
import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipStyle
import com.microsoft.fluentui.tokenized.controls.TextField
import com.microsoft.fluentui.tokenized.persona.Person
import com.microsoft.fluentui.tokenized.persona.PersonaChip
import com.microsoft.fluentui.peoplepicker.R
/**
* API to create a customized PeoplePicker for users to add a list of PersonaChips
*
* Whenever the user edits the text or a new PersonaChip is added onValueChange is called with the most up to date data
* with which developer is expected to update their state.
*
* PeoplePicker uses [PeoplePickerItemData] to represent a PersonaChip. This is a wrapper around [Person].
*
* Note: Use rememberPeoplePickerState function on selectedPeople list to create a rememberSaveable state for PeoplePicker.
*
* @param selectedPeopleList List of PersonaChips to be shown in PeoplePicker.
* @param onValueChange The callback that is triggered when the input service updates the text or [selectedPeopleList].
* An updated text and List of selectedPeople comes as a parameter of the callback
* @param modifier Optional modifier for the TextField
* @param onBackPress The callback that is triggered when the back button is pressed.
* @param onChipClick The callback that is triggered when a PersonaChip is clicked.
* @param onChipCloseClick The callback that is triggered when the close button of a PersonaChip is clicked.
* Note: use this callback to show the cancel button for a persona chip. To disable/not show the close button leave this callback as null.
* @param chipValidation The callback that is triggered when a PersonaChip is added. This callback is used to validate
* the PersonaChip before adding it to the list of selectedPeople.
* @param onTextEntered The callback that is triggered when the user clicks done on the keyboard.
* @param label String which acts as a description for the TextField.
* @param assistiveText String which assists users with the TextField
* @param errorString String to describe the error. TextField goes in error mode if this is provided.
* @param searchHint String to be shown as hint when the PeoplePicker is in rest state.
* @param leadingRestIcon Icon which is displayed when the textField is in rest state.
* @param leadingFocusIcon Icon which is displayed when the textField is in focus state.
* @param leadingIconContentDescription String which acts as content description for leading icon.
* @param trailingAccessoryIcon Icon which is displayed towards the end of textField and mainly
* acts as dismiss icon.
* @param peoplePickerContentDescription String which acts as content description for the PeoplePicker. Add content description for accessibility description.
* @param peoplePickerTokens Customization options for the PeoplePicker.
*/
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
@Composable
fun PeoplePicker(
selectedPeopleList: MutableList<PeoplePickerItemData> = mutableStateListOf(),
onValueChange: (String, MutableList<PeoplePickerItemData>) -> Unit,
modifier: Modifier = Modifier,
onBackPress: ((String, PeoplePickerItemData?) -> Unit)? = null,
onChipClick: ((PeoplePickerItemData) -> Unit)? = null,
onChipCloseClick: ((PeoplePickerItemData) -> Unit)? = null,
chipValidation: (Person) -> PersonaChipStyle = { PersonaChipStyle.Neutral },
onTextEntered: ((String) -> Unit)? = null,
label: String? = null,
assistiveText: String? = null,
errorString: String? = null,
searchHint: String? = null,
leadingRestIcon: ImageVector? = null,
leadingFocusIcon: ImageVector? = null,
leadingIconContentDescription: String? = null,
trailingAccessoryIcon: FluentIcon? = FluentIcon(
SearchBarIcons.Dismisscircle,
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_clear_text)
),
peoplePickerContentDescription: String? = null,
peoplePickerTokens: PeoplePickerTokens? = null
) {
val themeID =
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
val token = peoplePickerTokens
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PeoplePickerControlType] as PeoplePickerTokens
val peoplePickerInfo = PeoplePickerInfo()
val chipSpacing = token.chipSpacing(peoplePickerInfo = peoplePickerInfo)
var queryText by rememberSaveable { mutableStateOf("") }
var selectedPeopleListSize by rememberSaveable { mutableStateOf(0) }
var lastAddedPerson by rememberSaveable { mutableStateOf(Person()) }
var lastRemovedPerson by rememberSaveable { mutableStateOf(Person()) }
var isAdded: Boolean by rememberSaveable { mutableStateOf(true) }
var accessibilityAnnouncement by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
TextField(
modifier = modifier
.onKeyEvent {
if (it.key == Key.Backspace) {
if (onBackPress != null) {
onBackPress.invoke(queryText, selectedPeopleList.lastOrNull())
onValueChange(queryText, selectedPeopleList)
}
}
true
}
.focusRequester(focusRequester),
value = queryText,
onValueChange = {
queryText = it
onValueChange(queryText, selectedPeopleList)
},
hintText = searchHint,
label = label,
assistiveText = assistiveText,
errorString = errorString,
leadingFocusIcon = leadingFocusIcon,
leadingRestIcon = leadingRestIcon,
leadingIconContentDescription = leadingIconContentDescription,
trailingAccessoryIcon = trailingAccessoryIcon,
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(onDone = {
if (onTextEntered != null) {
onTextEntered.invoke(queryText)
queryText = ""
onValueChange(queryText, selectedPeopleList)
}
}),
textFieldContentDescription = peoplePickerContentDescription,
decorationBox = { innerTextField ->
Box(
Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.semantics { this.contentDescription = accessibilityAnnouncement },
contentAlignment = if (LocalLayoutDirection.current == LayoutDirection.Rtl)
Alignment.CenterEnd
else
Alignment.CenterStart
) {
Row {
if (selectedPeopleList.isNotEmpty()) {
selectedPeopleList.forEach {
PersonaChip(
person = it.person, selected = it.selected.value,
onCloseClick = if (onChipCloseClick != null) {
{
onChipCloseClick.invoke(it)
onValueChange(queryText, selectedPeopleList)
}
} else {
null
},
onClick = {
onChipClick?.invoke(it)
onValueChange(queryText, selectedPeopleList)
},
style = chipValidation(it.person)
)
Spacer(modifier = Modifier.width(chipSpacing))
}
focusRequester.requestFocus()
}
if (selectedPeopleListSize != selectedPeopleList.size) {
queryText = ""
onValueChange(queryText, selectedPeopleList)
isAdded = selectedPeopleListSize < selectedPeopleList.size
lastAddedPerson = selectedPeopleList.lastOrNull()?.person ?: Person()
accessibilityAnnouncement = if (isAdded) {
LocalContext.current.resources.getString(R.string.people_picker_accessibility_persona_added, lastAddedPerson.getLabel())
} else {
LocalContext.current.resources.getString(R.string.people_picker_accessibility_persona_removed, lastRemovedPerson.getLabel())
}
lastRemovedPerson = lastAddedPerson
}
selectedPeopleListSize = selectedPeopleList.size
Box {
if (queryText.isEmpty()) {
BasicText(
searchHint ?: "",
style = token.hintTextTypography(peoplePickerInfo)
.merge(
TextStyle(
color = token.hintColor(peoplePickerInfo)
)
)
)
}
innerTextField()
}
}
}
},
textFieldTokens = peoplePickerTokens
)
}
data class PeoplePickerItemData(
val person: Person,
var selected: MutableState<Boolean> = mutableStateOf(false)
)
@Composable
fun rememberPeoplePickerItemDataList(
initialValue: SnapshotStateList<PeoplePickerItemData> = mutableStateListOf(),
): SnapshotStateList<PeoplePickerItemData> {
return rememberSaveable(
saver = Saver(
save = {
val saved = mutableListOf<Map<String, Any?>>()
it.forEach { itemData ->
saved.add(
mapOf(
"selectedKey" to itemData.selected.value,
"firstName" to itemData.person.firstName,
"lastName" to itemData.person.lastName,
"email" to itemData.person.email,
"image" to itemData.person.image,
"imageBitmap" to itemData.person.imageBitmap,
"isActive" to itemData.person.isActive,
"isOOO" to itemData.person.isOOO,
"status" to itemData.person.status
)
)
}
saved
},
restore = { restored ->
val list = mutableStateListOf<PeoplePickerItemData>()
restored.forEach { item ->
list.add(
PeoplePickerItemData(
person = Person(
firstName = item["firstName"] as String,
lastName = item["lastName"] as String,
email = item["email"] as String?,
image = item["image"] as Int?,
imageBitmap = item["imageBitmap"] as ImageBitmap?,
isActive = item["isActive"] as Boolean,
isOOO = item["isOOO"] as Boolean,
status = item["status"] as AvatarStatus
),
selected = mutableStateOf(item["selectedKey"] as Boolean)
)
)
}
list
}
)
) {
initialValue
}
}