V2 Peoplepicker implementation (#445)
* peoplepicker impl * Peoplepicker Impl * trailing content for ppl picker * activity changes * added peoplepicker remember saveable to save state * using textfield for pplPicker * removing textfield dependency * adding api doc * review changes and adding UI testcases * personachip accessibility * refactoring tokens file * removing unwanted files * ui test updated --------- Co-authored-by: PraveenKumar <pyeruva@microsoft.com>
This commit is contained in:
Родитель
5292b576a3
Коммит
802d07ce53
|
@ -18,6 +18,7 @@ import org.junit.runners.Suite
|
|||
V2DrawerActivityUITest::class,
|
||||
V2LabelUITest::class,
|
||||
V2ListItemActivityUITest::class,
|
||||
V2PeoplePickerUITest::class,
|
||||
V2PersonaUITest::class,
|
||||
V2PersonaChipActivityUITest::class,
|
||||
V2PersonaListActivityUITest::class,
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
package com.microsoft.fluentuidemo.demos
|
||||
|
||||
import android.view.KeyEvent.KEYCODE_BACK
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.NativeKeyEvent
|
||||
import androidx.compose.ui.input.key.nativeKeyCode
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
import androidx.compose.ui.test.assertIsNotFocused
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performKeyPress
|
||||
import androidx.compose.ui.test.printToLog
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.PeoplePicker
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.PeoplePickerItemData
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.rememberPeoplePickerItemDataList
|
||||
import com.microsoft.fluentui.tokenized.persona.Person
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class V2PeoplePickerUITest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun testV2PeoplePicker() {
|
||||
composeTestRule.setContent {
|
||||
PeoplePicker(modifier = Modifier.testTag("V2PeoplePicker"), onValueChange = { _, _ ->
|
||||
|
||||
})
|
||||
}
|
||||
composeTestRule.onRoot().printToLog("testPeoplePickerChip")
|
||||
val peoplePicker = composeTestRule.onNodeWithTag("V2PeoplePicker")
|
||||
peoplePicker.assertExists()
|
||||
peoplePicker.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testV2PeoplePickerTextField() {
|
||||
composeTestRule.setContent {
|
||||
PeoplePicker(modifier = Modifier.testTag("V2PeoplePicker"), onValueChange = { _, _ ->
|
||||
|
||||
})
|
||||
}
|
||||
composeTestRule.onRoot().printToLog("testPeoplePickerChip")
|
||||
val textField = composeTestRule.onNodeWithTag("Text field")
|
||||
textField.assertExists()
|
||||
textField.assertIsDisplayed()
|
||||
textField.assertIsNotFocused()
|
||||
textField.performClick()
|
||||
textField.assertIsFocused()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPeoplePickerChip() {
|
||||
|
||||
composeTestRule.setContent {
|
||||
PeoplePicker(
|
||||
modifier = Modifier.testTag("V2PeoplePicker"),
|
||||
onValueChange = { _, _ ->
|
||||
},
|
||||
selectedPeopleList = mutableListOf(
|
||||
PeoplePickerItemData(
|
||||
Person("firstName"),
|
||||
mutableStateOf(false)
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
val peoplePickerChip = composeTestRule.onNodeWithContentDescription("firstName", true)
|
||||
peoplePickerChip.assertExists()
|
||||
peoplePickerChip.assertIsDisplayed()
|
||||
peoplePickerChip.assertHasClickAction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPeoplePickerChipActions() {
|
||||
|
||||
composeTestRule.setContent {
|
||||
var selectedPeopleList = rememberPeoplePickerItemDataList()
|
||||
selectedPeopleList.add(
|
||||
PeoplePickerItemData(
|
||||
Person("firstName"),
|
||||
mutableStateOf(false)
|
||||
)
|
||||
)
|
||||
PeoplePicker(
|
||||
modifier = Modifier.testTag("V2PeoplePicker"),
|
||||
onValueChange = { _, _ ->
|
||||
},
|
||||
selectedPeopleList = selectedPeopleList,
|
||||
onChipClick = {
|
||||
it.selected.value = !it.selected.value
|
||||
},
|
||||
onChipCloseClick = {
|
||||
selectedPeopleList.remove(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val peoplePickerChip = composeTestRule.onNodeWithText("firstName", true, useUnmergedTree = true)
|
||||
peoplePickerChip.performClick()
|
||||
val cancelButton = composeTestRule.onNodeWithContentDescription("Close", useUnmergedTree = true)
|
||||
cancelButton.assertExists()
|
||||
cancelButton.assertIsDisplayed()
|
||||
cancelButton.assertHasClickAction()
|
||||
cancelButton.performClick()
|
||||
peoplePickerChip.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Test
|
||||
fun testPeoplePickerBackPress() {
|
||||
composeTestRule.setContent {
|
||||
var selectedPeopleList = rememberPeoplePickerItemDataList()
|
||||
selectedPeopleList.add(
|
||||
PeoplePickerItemData(
|
||||
Person("firstName"),
|
||||
mutableStateOf(false)
|
||||
)
|
||||
)
|
||||
PeoplePicker(
|
||||
modifier = Modifier.testTag("V2PeoplePicker"),
|
||||
onValueChange = { _, _ ->
|
||||
},
|
||||
selectedPeopleList = selectedPeopleList,
|
||||
onBackPress = { _, it ->
|
||||
selectedPeopleList.remove(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
val textField = composeTestRule.onNodeWithTag("Text field")
|
||||
textField.assertExists()
|
||||
textField.assertIsDisplayed()
|
||||
val peoplePickerChip = composeTestRule.onNodeWithText("firstName", true, useUnmergedTree = true)
|
||||
peoplePickerChip.assertExists()
|
||||
peoplePickerChip.assertIsDisplayed()
|
||||
textField.performClick()
|
||||
textField.performKeyPress(
|
||||
KeyEvent(
|
||||
NativeKeyEvent(
|
||||
KEYCODE_BACK,
|
||||
Key.Backspace.nativeKeyCode
|
||||
)
|
||||
)
|
||||
)
|
||||
peoplePickerChip.assertDoesNotExist()
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@
|
|||
<activity android:name="com.microsoft.fluentuidemo.demos.V2ListItemActivity" />
|
||||
<activity android:name="com.microsoft.fluentuidemo.demos.V2MenuActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity android:name="com.microsoft.fluentuidemo.demos.V2PeoplePickerActivity" />
|
||||
<activity android:name="com.microsoft.fluentuidemo.demos.V2PersonaActivity" />
|
||||
<activity android:name="com.microsoft.fluentuidemo.demos.V2PersonaChipActivity" />
|
||||
<activity android:name="com.microsoft.fluentuidemo.demos.V2PersonaListActivity" />
|
||||
|
|
|
@ -25,6 +25,7 @@ const val V2DIALOG = "V2 Dialog"
|
|||
const val V2DRAWER = "V2 Drawer"
|
||||
const val V2LIST_ITEM = "V2 ListItem"
|
||||
const val V2MENU = "V2 Menu"
|
||||
const val V2PEOPLE_PICKER = "V2 People Picker"
|
||||
const val V2PERSONA = "V2 Persona"
|
||||
const val V2PERSONA_CHIP = "V2 PersonaChip"
|
||||
const val V2PERSONA_LIST = "V2 PersonaList"
|
||||
|
@ -82,6 +83,7 @@ val DEMOS = arrayListOf(
|
|||
Demo(V2LABEL, V2LabelActivity::class),
|
||||
Demo(V2LIST_ITEM, V2ListItemActivity::class),
|
||||
Demo(V2MENU, V2MenuActivity::class),
|
||||
Demo(V2PEOPLE_PICKER, V2PeoplePickerActivity::class),
|
||||
Demo(V2PERSONA, V2PersonaActivity::class),
|
||||
Demo(V2PERSONA_CHIP, V2PersonaChipActivity::class),
|
||||
Demo(V2PERSONA_LIST, V2PersonaListActivity::class),
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
package com.microsoft.fluentuidemo.demos
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.fluentui.theme.FluentTheme
|
||||
import com.microsoft.fluentui.theme.token.FluentAliasTokens
|
||||
import com.microsoft.fluentui.theme.token.Icon
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipStyle
|
||||
import com.microsoft.fluentui.tokenized.controls.Label
|
||||
import com.microsoft.fluentui.tokenized.notification.Snackbar
|
||||
import com.microsoft.fluentui.tokenized.notification.SnackbarState
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.PeoplePicker
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.PeoplePickerItemData
|
||||
import com.microsoft.fluentui.tokenized.peoplepicker.rememberPeoplePickerItemDataList
|
||||
import com.microsoft.fluentui.tokenized.persona.AvatarGroup
|
||||
import com.microsoft.fluentui.tokenized.persona.Group
|
||||
import com.microsoft.fluentui.tokenized.persona.Person
|
||||
import com.microsoft.fluentui.tokenized.persona.Persona
|
||||
import com.microsoft.fluentui.tokenized.persona.PersonaList
|
||||
import com.microsoft.fluentuidemo.DemoActivity
|
||||
import com.microsoft.fluentuidemo.R
|
||||
import com.microsoft.fluentuidemo.databinding.V2ActivityComposeBinding
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class V2PeoplePickerActivity : DemoActivity() {
|
||||
override val contentNeedsScrollableContainer: Boolean
|
||||
get() = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val v2ActivityComposeBinding = V2ActivityComposeBinding.inflate(
|
||||
LayoutInflater.from(container.context),
|
||||
container,
|
||||
true
|
||||
)
|
||||
v2ActivityComposeBinding.composeHere.setContent {
|
||||
FluentTheme {
|
||||
CreatePeoplePickerActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreatePeoplePickerActivity() {
|
||||
val people = mutableListOf(
|
||||
Person(
|
||||
"Allan", "Munger",
|
||||
image = R.drawable.avatar_allan_munger,
|
||||
email = "allan.munger@xyz.com",
|
||||
isActive = true
|
||||
),
|
||||
Person(
|
||||
"Amanda", "Brady",
|
||||
email = "amanda.brady@xyz.com",
|
||||
isActive = false, status = AvatarStatus.Offline
|
||||
),
|
||||
Person(
|
||||
"Abhay", "Singh",
|
||||
email = "abhay.singh@xyz.com",
|
||||
isActive = true, status = AvatarStatus.DND, isOOO = true
|
||||
),
|
||||
Person(
|
||||
"Carlos", "Slathery",
|
||||
email = "carlos.slathery@xyz.com",
|
||||
isActive = false, status = AvatarStatus.Busy, isOOO = true
|
||||
),
|
||||
Person(
|
||||
"Celeste", "Burton",
|
||||
email = "celeste.burton@xyz.com",
|
||||
image = R.drawable.avatar_celeste_burton,
|
||||
isActive = true, status = AvatarStatus.Away
|
||||
),
|
||||
Person(
|
||||
"Ankit", "Gupta",
|
||||
email = "ankit.gupta@xyz.com",
|
||||
isActive = true, status = AvatarStatus.Unknown
|
||||
),
|
||||
Person(
|
||||
"Miguel", "Garcia",
|
||||
email = "miguel.garcia@xyz.com",
|
||||
image = R.drawable.avatar_miguel_garcia,
|
||||
isActive = true, status = AvatarStatus.Blocked
|
||||
)
|
||||
)
|
||||
var selectedPeopleList = rememberPeoplePickerItemDataList()
|
||||
|
||||
|
||||
val snackbarState = remember { SnackbarState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
var suggested by rememberSaveable { mutableStateOf(people) }
|
||||
var suggestedPersonaList = mutableListOf<Persona>()
|
||||
var selectedPersonList = mutableListOf<Person>()
|
||||
var errorPeopleList = mutableListOf<Person>()
|
||||
var assistiveText by rememberSaveable { mutableStateOf(true) }
|
||||
var errorText by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
PeoplePicker(
|
||||
onValueChange = { query, selectedPerson ->
|
||||
scope.launch {
|
||||
suggested = people.filter {
|
||||
it.firstName.lowercase().contains(query.lowercase()) ||
|
||||
it.lastName.lowercase().contains(query.lowercase())
|
||||
} as MutableList<Person>
|
||||
}
|
||||
},
|
||||
selectedPeopleList = selectedPeopleList,
|
||||
chipValidation = {
|
||||
if (!it.email.isNullOrBlank()) {
|
||||
if (it.email?.contains("@") == true)
|
||||
PersonaChipStyle.Neutral
|
||||
else {
|
||||
errorText = true
|
||||
errorPeopleList.add(it)
|
||||
PersonaChipStyle.Danger
|
||||
}
|
||||
|
||||
} else {
|
||||
errorText = true
|
||||
errorPeopleList.add(it)
|
||||
PersonaChipStyle.Danger
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
onChipClick = {
|
||||
scope.launch {
|
||||
it.selected.value = !it.selected.value
|
||||
snackbarState.showSnackbar("Clicked ${it.person.firstName} ${it.person.lastName}")
|
||||
}
|
||||
},
|
||||
onChipCloseClick = {
|
||||
selectedPeopleList.remove(it)
|
||||
run outer@{
|
||||
errorPeopleList.forEach { errorPerson ->
|
||||
if (errorPerson == it.person) {
|
||||
errorPeopleList.remove(errorPerson)
|
||||
return@outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorPeopleList.isEmpty())
|
||||
errorText = false
|
||||
},
|
||||
onTextEntered = { queryText ->
|
||||
if (queryText.isNotBlank() && queryText.isNotEmpty()) {
|
||||
selectedPeopleList.add(
|
||||
PeoplePickerItemData(
|
||||
Person(queryText, ""),
|
||||
mutableStateOf(false)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onBackPress = { queryText, it ->
|
||||
if (queryText.isEmpty() && it != null) {
|
||||
if (!it.selected.value) {
|
||||
it.selected.value = !it.selected.value
|
||||
} else {
|
||||
selectedPeopleList.remove(it)
|
||||
errorPeopleList.forEach { errorPerson ->
|
||||
if (errorPerson == it.person)
|
||||
errorPeopleList.remove(errorPerson)
|
||||
}
|
||||
if (errorPeopleList.isEmpty())
|
||||
errorText = false
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingAccessoryContent = {
|
||||
Icon(
|
||||
Icons.Filled.Person,
|
||||
contentDescription = "Person",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
},
|
||||
trailingAccessoryContent = {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Add",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
},
|
||||
label = "People Picker",
|
||||
searchHint = "Search People",
|
||||
assistiveText = if (assistiveText) "This is a sample Assistive Text" else null,
|
||||
errorString = if (errorText) "This is a sample Error text" else null,
|
||||
)
|
||||
}
|
||||
|
||||
suggested.forEach outer@{
|
||||
selectedPeopleList.forEach { selectedPerson ->
|
||||
if (selectedPerson.person.email == it.email) {
|
||||
return@outer
|
||||
}
|
||||
}
|
||||
suggestedPersonaList.add(
|
||||
Persona(
|
||||
it,
|
||||
"${it.firstName} ${it.lastName}",
|
||||
subTitle = it.email,
|
||||
onClick = {
|
||||
selectedPeopleList.add(PeoplePickerItemData(it, mutableStateOf(false)))
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar(
|
||||
"Added ${it.firstName} ${it.lastName}",
|
||||
enableDismiss = true
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
selectedPeopleList.forEach {
|
||||
selectedPersonList.add(it.person)
|
||||
}
|
||||
Column {
|
||||
PersonaList(personas = suggestedPersonaList, modifier = Modifier.padding(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Label(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = "Selected People from people picker",
|
||||
textStyle = FluentAliasTokens.TypographyTokens.Body1
|
||||
)
|
||||
AvatarGroup(group = Group(selectedPersonList), modifier = Modifier.padding(8.dp))
|
||||
Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
|
||||
Snackbar(snackbarState, Modifier.padding(bottom = 12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ open class ControlTokens : IControlTokens {
|
|||
LinearProgressIndicator,
|
||||
ListItem,
|
||||
Menu,
|
||||
PeoplePicker,
|
||||
PersonaChip,
|
||||
PillButton,
|
||||
PillBar,
|
||||
|
@ -97,6 +98,7 @@ open class ControlTokens : IControlTokens {
|
|||
ControlType.ListItem -> ListItemTokens()
|
||||
ControlType.Menu -> MenuTokens()
|
||||
ControlType.PersonaChip -> PersonaChipTokens()
|
||||
ControlType.PeoplePicker -> PeoplePickerTokens()
|
||||
ControlType.PillButton -> PillButtonTokens()
|
||||
ControlType.PillBar -> PillBarTokens()
|
||||
ControlType.PillSwitch -> PillSwitchTokens()
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
package com.microsoft.fluentui.theme.token.controlTokens
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.microsoft.fluentui.theme.FluentTheme
|
||||
import com.microsoft.fluentui.theme.token.ControlInfo
|
||||
import com.microsoft.fluentui.theme.token.FluentAliasTokens
|
||||
import com.microsoft.fluentui.theme.token.FluentGlobalTokens
|
||||
import com.microsoft.fluentui.theme.token.IControlToken
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class PeoplePickerInfo(
|
||||
val isStatusError: Boolean = false,
|
||||
val isFocused: Boolean = false
|
||||
) : ControlInfo
|
||||
|
||||
@Parcelize
|
||||
open class PeoplePickerTokens : IControlToken, Parcelable {
|
||||
|
||||
@Composable
|
||||
open fun backgroundBrush(peoplePickerInfo: PeoplePickerInfo): Brush {
|
||||
return SolidColor(
|
||||
FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
|
||||
themeMode = FluentTheme.themeMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun leadingAccessorySpacing(peoplePickerInfo: PeoplePickerInfo): Dp {
|
||||
return FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size160)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun trailingAccessoryPadding(peoplePickerInfo: PeoplePickerInfo): PaddingValues {
|
||||
return PaddingValues(end = FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size160))
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun chipSpacing(peoplePickerInfo: PeoplePickerInfo): Dp {
|
||||
return FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size80)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun minHeight(peoplePickerInfo: PeoplePickerInfo): Dp {
|
||||
return FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size480)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun textColor(peoplePickerInfo: PeoplePickerInfo): Color {
|
||||
return FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
|
||||
themeMode = FluentTheme.themeMode
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun textFieldTypography(peoplePickerInfo: PeoplePickerInfo): TextStyle {
|
||||
return FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1]
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun cursorBrush(peoplePickerInfo: PeoplePickerInfo): Brush {
|
||||
return SolidColor(
|
||||
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value(
|
||||
themeMode = FluentTheme.themeMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun labelTypography(peoplePickerInfo: PeoplePickerInfo): TextStyle {
|
||||
return FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Caption2]
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun labelColor(peoplePickerInfo: PeoplePickerInfo): Color {
|
||||
return if (peoplePickerInfo.isStatusError)
|
||||
FluentTheme.aliasTokens.errorAndStatusColor[FluentAliasTokens.ErrorAndStatusColorTokens.DangerForeground1].value()
|
||||
else if (peoplePickerInfo.isFocused)
|
||||
FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value()
|
||||
else
|
||||
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value()
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun assistiveTextColor(peoplePickerInfo: PeoplePickerInfo): Color {
|
||||
return if (peoplePickerInfo.isStatusError)
|
||||
FluentTheme.aliasTokens.errorAndStatusColor[FluentAliasTokens.ErrorAndStatusColorTokens.DangerForeground1].value()
|
||||
else
|
||||
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value()
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun assistiveTextTypography(peoplePickerInfo: PeoplePickerInfo): TextStyle {
|
||||
return FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Caption2]
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun assistiveTextPadding(peoplePickerInfo: PeoplePickerInfo): PaddingValues {
|
||||
return PaddingValues(
|
||||
top = FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size40),
|
||||
bottom = FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size40)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun hintColor(peoplePickerInfo: PeoplePickerInfo): Color {
|
||||
return FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value()
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun hintTextTypography(peoplePickerInfo: PeoplePickerInfo): TextStyle {
|
||||
return FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1]
|
||||
}
|
||||
|
||||
open fun strokeWidth(peoplePickerInfo: PeoplePickerInfo): Dp =
|
||||
FluentGlobalTokens.strokeWidth(FluentGlobalTokens.StrokeWidthTokens.StrokeWidth05)
|
||||
|
||||
@Composable
|
||||
open fun dividerBrush(peoplePickerInfo: PeoplePickerInfo): Brush {
|
||||
return SolidColor(
|
||||
if (peoplePickerInfo.isStatusError)
|
||||
FluentTheme.aliasTokens.errorAndStatusColor[FluentAliasTokens.ErrorAndStatusColorTokens.DangerForeground1].value()
|
||||
else if (peoplePickerInfo.isFocused)
|
||||
FluentTheme.aliasTokens.brandStroke[FluentAliasTokens.BrandStrokeColorTokens.BrandStroke1].value()
|
||||
else
|
||||
FluentTheme.aliasTokens.neutralStrokeColor[FluentAliasTokens.NeutralStrokeColorTokens.Stroke2].value()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -238,4 +238,12 @@ open class PersonaChipTokens : IControlToken, Parcelable {
|
|||
open fun avatarSize(personaChipInfo: PersonaChipControlInfo): AvatarSize {
|
||||
return AvatarSize.Size16
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun maxHeight(personaChipInfo: PersonaChipControlInfo): Dp {
|
||||
return when (personaChipInfo.size) {
|
||||
Small -> FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size200)
|
||||
Medium -> FluentGlobalTokens.size(FluentGlobalTokens.SizeTokens.Size240)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ android {
|
|||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
|
@ -31,6 +32,20 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
useIR = true
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
|
@ -59,6 +74,11 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
|
||||
implementation "com.splitwise:tokenautocomplete:$tokenautocompleteVersion"
|
||||
|
||||
implementation("androidx.compose.foundation:foundation:$composeVersion")
|
||||
implementation("androidx.compose.runtime:runtime:$composeVersion")
|
||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
||||
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||
|
|
|
@ -0,0 +1,362 @@
|
|||
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.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
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.rememberCoroutineScope
|
||||
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.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
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.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.fluentui.theme.FluentTheme
|
||||
import com.microsoft.fluentui.theme.token.ControlTokens
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.DividerInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.DividerTokens
|
||||
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.divider.Divider
|
||||
import com.microsoft.fluentui.tokenized.persona.Person
|
||||
import com.microsoft.fluentui.tokenized.persona.PersonaChip
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 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 leadingAccessoryContent The content to be placed towards the start of PeoplePicker.
|
||||
* @param trailingAccessoryContent The content to be placed towards the end of PeoplePicker.
|
||||
* @param label String which acts as a description for the PeoplePicker.
|
||||
* @param assistiveText String which assists users with the PeoplePicker
|
||||
* @param errorString String to describe the error. PeoplePicker goes in error mode if this is provided.
|
||||
* @param searchHint String to be shown as hint when the PeoplePicker is in rest state.
|
||||
* @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,
|
||||
leadingAccessoryContent: (@Composable () -> Unit)? = null,
|
||||
trailingAccessoryContent: (@Composable () -> Unit)? = null,
|
||||
label: String? = null,
|
||||
assistiveText: String? = null,
|
||||
errorString: String? = null,
|
||||
searchHint: 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.PeoplePicker] as PeoplePickerTokens
|
||||
|
||||
var isFocused: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||
val peoplePickerInfo = PeoplePickerInfo(
|
||||
isStatusError = !errorString.isNullOrBlank(),
|
||||
isFocused = isFocused,
|
||||
)
|
||||
val leadingAccessorySpacing = token.leadingAccessorySpacing(peoplePickerInfo = peoplePickerInfo)
|
||||
val trailingAccessoryPadding =
|
||||
token.trailingAccessoryPadding(peoplePickerInfo = peoplePickerInfo)
|
||||
val chipSpacing = token.chipSpacing(peoplePickerInfo = peoplePickerInfo)
|
||||
val textTypography = token.textFieldTypography(peoplePickerInfo)
|
||||
val textColor = token.textColor(peoplePickerInfo = peoplePickerInfo)
|
||||
val cursorBrush = token.cursorBrush(peoplePickerInfo = peoplePickerInfo)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
var queryText by rememberSaveable { mutableStateOf("") }
|
||||
var selectedPeopleListSize by rememberSaveable { mutableStateOf(0) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.onFocusChanged { focusState ->
|
||||
when {
|
||||
focusState.isFocused -> {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (leadingAccessoryContent != null) {
|
||||
leadingAccessoryContent()
|
||||
Spacer(modifier = Modifier.width(leadingAccessorySpacing))
|
||||
}
|
||||
Column {
|
||||
if (!label.isNullOrBlank()) {
|
||||
Spacer(Modifier.requiredHeight(12.dp))
|
||||
BasicText(
|
||||
label,
|
||||
style = token.labelTypography(peoplePickerInfo).merge(
|
||||
TextStyle(
|
||||
color = token.labelColor(peoplePickerInfo)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(token.minHeight(peoplePickerInfo = peoplePickerInfo)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.onKeyEvent {
|
||||
if (it.key == Key.Backspace) {
|
||||
if (onBackPress != null) {
|
||||
onBackPress.invoke(queryText, selectedPeopleList.lastOrNull())
|
||||
onValueChange(queryText, selectedPeopleList)
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
if (selectedPeopleListSize < selectedPeopleList.size) {
|
||||
queryText = ""
|
||||
onValueChange(queryText, selectedPeopleList)
|
||||
}
|
||||
selectedPeopleListSize = selectedPeopleList.size
|
||||
BasicTextField(
|
||||
value = queryText,
|
||||
onValueChange = {
|
||||
queryText = it
|
||||
onValueChange(queryText, selectedPeopleList)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (onTextEntered != null) {
|
||||
onTextEntered.invoke(queryText)
|
||||
queryText = ""
|
||||
onValueChange(queryText, selectedPeopleList)
|
||||
}
|
||||
}),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.onFocusEvent { focusState ->
|
||||
when {
|
||||
focusState.isFocused -> {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.semantics { contentDescription = searchHint ?: "" }
|
||||
.testTag("Text field"),
|
||||
textStyle = textTypography.merge(
|
||||
TextStyle(
|
||||
color = textColor,
|
||||
textDirection = TextDirection.ContentOrLtr
|
||||
)
|
||||
),
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = if (LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
Alignment.CenterEnd
|
||||
else
|
||||
Alignment.CenterStart
|
||||
) {
|
||||
if (queryText.isEmpty()) {
|
||||
BasicText(
|
||||
searchHint ?: "",
|
||||
style = token.hintTextTypography(peoplePickerInfo)
|
||||
.merge(
|
||||
TextStyle(
|
||||
color = token.hintColor(peoplePickerInfo)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
innerTextField()
|
||||
},
|
||||
cursorBrush = cursorBrush
|
||||
)
|
||||
}
|
||||
if (trailingAccessoryContent != null) {
|
||||
Box(modifier = Modifier.padding(trailingAccessoryPadding)) {
|
||||
trailingAccessoryContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(
|
||||
height = token.strokeWidth(peoplePickerInfo),
|
||||
dividerToken = object : DividerTokens() {
|
||||
@Composable
|
||||
override fun verticalPadding(dividerInfo: DividerInfo): PaddingValues {
|
||||
return PaddingValues(0.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun dividerBrush(dividerInfo: DividerInfo): Brush =
|
||||
token.dividerBrush(peoplePickerInfo)
|
||||
}
|
||||
)
|
||||
if (!assistiveText.isNullOrBlank() || !errorString.isNullOrBlank()) {
|
||||
BasicText(
|
||||
errorString ?: assistiveText!!,
|
||||
style = token.assistiveTextTypography(peoplePickerInfo).merge(
|
||||
TextStyle(
|
||||
color = token.assistiveTextColor(peoplePickerInfo)
|
||||
)
|
||||
),
|
||||
modifier = Modifier.padding(token.assistiveTextPadding(peoplePickerInfo))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
var 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
|
||||
}
|
||||
}
|
|
@ -3,7 +3,12 @@ package com.microsoft.fluentui.tokenized.persona
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -16,6 +21,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.fluentui.persona.R
|
||||
|
@ -83,17 +90,23 @@ fun PersonaChip(
|
|||
token.avatarToTextSpacing(personaChipInfo = personaChipInfo)
|
||||
val cornerRadius =
|
||||
token.cornerRadius(personaChipInfo = personaChipInfo)
|
||||
val maxHeight =
|
||||
token.maxHeight(personaChipInfo = personaChipInfo)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.heightIn(max = maxHeight)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick ?: {},
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
.then(if (onCloseClick != null && selected) Modifier else Modifier.clearAndSetSemantics {
|
||||
this.contentDescription = person.getLabel()
|
||||
})
|
||||
)
|
||||
{
|
||||
Row(
|
||||
|
@ -120,7 +133,7 @@ fun PersonaChip(
|
|||
tint = textColor
|
||||
)
|
||||
} else {
|
||||
Avatar(person = person, size = avatarSize)
|
||||
Avatar(modifier = Modifier.clearAndSetSemantics { }, person = person, size = avatarSize)
|
||||
}
|
||||
}
|
||||
BasicText(
|
||||
|
|
Загрузка…
Ссылка в новой задаче