-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Compose: Add DaxTextField #7247
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
base: develop
Are you sure you want to change the base?
Conversation
| * Figma reference: https://www.figma.com/design/BOHDESHODUXK7wSRNBOHdu/%F0%9F%A4%96-Android-Components?m=auto&node-id=3202-5150 | ||
| */ | ||
| @Composable | ||
| fun DaxSecureTextField( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be a separate composable as it's using a OutlinedSecureTextField underneath instead of the regular OutlinedTextField
| enum class DaxTextFieldLineLimits { | ||
| /** | ||
| * The TextField will take up a single line and will scroll horizontally if the text is too long. | ||
| */ | ||
| SingleLine, | ||
|
|
||
| /** | ||
| * The TextField will start with a one line height and then expand vertically as needed to accommodate the text. | ||
| */ | ||
| MultiLine, | ||
|
|
||
| /** | ||
| * The TextField will always take at least 3 lines of height and then expand vertically as needed based on the input. | ||
| */ | ||
| Form, | ||
|
|
||
| ; | ||
|
|
||
| /** | ||
| * Converts this [DaxTextFieldLineLimits] to a [TextFieldLineLimits] used by the underlying TextField. | ||
| */ | ||
| fun toLineLimits(): TextFieldLineLimits = when (this) { | ||
| SingleLine -> TextFieldLineLimits.SingleLine | ||
| MultiLine -> TextFieldLineLimits.MultiLine(minHeightInLines = 1) | ||
| Form -> TextFieldLineLimits.MultiLine(minHeightInLines = 3) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially had three separate composables that called a base composable underneath: DaxSingleLineTextField, DaxMultiLineTextField and DaxFormTextField. But all three were exactly the same, with same arguments, only difference was what they used for TextFieldLineLimits, so I thought it was clearer this way.
It was also a bit hard to make and maintain the same changes across four composables, as any change to the base one needs to be propagated.
If there is a strong preference towards having separate composables, I can easily split this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer having one instead, good call!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another approach with Compose is to define one generic component that will handle for us all the UI customisation and then, propose easy to use variant components for dedicated cases. It depends a lot the experience we want to propose to the team.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GerardPaligot I agree, that was my initial approach. But there was no difference between the components apart from the number of lines, so the maintenance overhead of having three separate composables vs one argument didn't seem worth it. However, I'm okay with going with separate composables if that is our approach we want to take.
The password text field is a separate composable as it's has a number of differences, including a different base composable.
| */ | ||
| @Composable | ||
| fun DaxTextField( | ||
| state: TextFieldState, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to go with the state-based text fields as that is the recommended approach. There is a synchronization issue with the old value-based text fields.
malmstein
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very nice work, just a couple of nits. I’ll leave it to @mikescamell for the final approval.
| window = colorResource(R.color.white), | ||
| destructive = colorResource(R.color.alertRedOnLightDefault), | ||
| lines = colorResource(R.color.black9), | ||
| borders = colorResource(R.color.black30), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
borders is too generic, so far it only applies to the input field. how do you feel about doing inputFieldBorders instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to keep it generic. This list of colors should be enough to build any component that follows ADS. I don't think we should add one-off colors to this. But I don't feel strongly about this, especially since it doesn't seem any other component has borders and uses T-Control/Border-Primary color as it's named in Figma.
One other option is that each component specifies it's own colors. We kinda already do that for the DaxText. So for text field it would be something like:
textField = DuckDuckGoTextFieldColors(
text: Color,
error: Color,
borders: Color,
...
)
We could either provide all needed colors or just the ones that are not generic.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends a lot on the design spec. This borders is used only in the text field or every time we need to use a border, they'll use the exact same border color?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GerardPaligot I'm not familiar enough with the ADS to answer that confidently. I didn't see any other references to this color in Figma page for ADS components. For now, the borders is only used by the text field. But keep in mind that the text field is only the second Compose component we have 😅
| enum class DaxTextFieldLineLimits { | ||
| /** | ||
| * The TextField will take up a single line and will scroll horizontally if the text is too long. | ||
| */ | ||
| SingleLine, | ||
|
|
||
| /** | ||
| * The TextField will start with a one line height and then expand vertically as needed to accommodate the text. | ||
| */ | ||
| MultiLine, | ||
|
|
||
| /** | ||
| * The TextField will always take at least 3 lines of height and then expand vertically as needed based on the input. | ||
| */ | ||
| Form, | ||
|
|
||
| ; | ||
|
|
||
| /** | ||
| * Converts this [DaxTextFieldLineLimits] to a [TextFieldLineLimits] used by the underlying TextField. | ||
| */ | ||
| fun toLineLimits(): TextFieldLineLimits = when (this) { | ||
| SingleLine -> TextFieldLineLimits.SingleLine | ||
| MultiLine -> TextFieldLineLimits.MultiLine(minHeightInLines = 1) | ||
| Form -> TextFieldLineLimits.MultiLine(minHeightInLines = 3) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer having one instead, good call!
| fun DaxTextField( | ||
| state: TextFieldState, | ||
| modifier: Modifier = Modifier, | ||
| hint: String? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning: We need to take care about the naming here. The Compose TextField has two native parameters: placeholder and label. placeholder is displayed when you click on the text field but the user didn't enter anything yet and label is displayed at the beginning in the text field and is pushed at the top when the user click on the component.
hint seems too vague and according to the Figma specification, it seems that the placeholder isn't documented in our component specification. Maybe we should ask to a designer what they want and impact the api contract here according to their feedback?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GerardPaligot I'm open to changing the name. I was on the fence between hint and label, but went with hint for two reasons: it's also used in the old DaxTextInput component and combines both label and placeholder since we're not using placeholder.
We're not using the placeholder parameter, but only rely on the label parameter, because it matches how our current View-based TextInput component works. The label is displayed inside the text field when it's empty and not focused, and animates to the top of the text field when it's focused or has text.
| keyboardOptions: KeyboardOptions = DaxTextFieldDefaults.TextKeyboardOptions, | ||
| inputTransformation: InputTransformation? = null, | ||
| onClick: (() -> Unit)? = null, | ||
| onFocusChanged: ((Boolean) -> Unit)? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Instead of declaring a callback, maybe we can expose a interactionSource: MutableInteractionSource to observe a focus change on the consumer side (interactionSource.collectIsFocusedAsState())?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GerardPaligot I like the idea, could you expand a bit more what you had in mind? We're using Modifier.onFocusChanged to observe and dispatch changes and to detect clicks on the text field.
| trailingIcon: DaxTextFieldTrailingIcon? = null, | ||
| keyboardOptions: KeyboardOptions = DaxTextFieldDefaults.TextKeyboardOptions, | ||
| inputTransformation: InputTransformation? = null, | ||
| onClick: (() -> Unit)? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: It seems that this parameter is used when you click on the text field (L115) and when you click on the trailing icon (L142), can we distinguish these two sub components?
Suggestion: Or do we really need to add this callback on the root component and maybe add an api slot for the trailing icon where we'll declare the callback when we need to observe user click on this sub component? This will allow us to propose an api contract much more cleaner on the text field component and a better responsibility definition for every sub components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GerardPaligot Great question! I initially had a composable slot for the trailing icon so consumers could put their own IconButton there as that's the Compose way. However, we would then need to set up lint rules to ensure people are not putting other elements there or using wrong styling. It also puts more burden on the consumers as they need to be aware of what to put in the slot. But I'm open to using that or other approaches if we think it would be better. It depends on what we want to do with our Compose ADS.
I went with a single onClick() callback based on how the old TextInput works. There are four possible options:
- there is no icon and text field is not clickable
- icon is clickable
- text field is clickable
- both are clickable
I felt having two separate callbacks could be a bit confusing, but I'm open to changing that.
| lineLimits: DaxTextFieldLineLimits = DaxTextFieldLineLimits.MultiLine, | ||
| enabled: Boolean = true, | ||
| editable: Boolean = true, | ||
| clickable: Boolean = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: enabled, editable and clickable are very confusing and I didn't find a clear design spec about these 3 states. Are they really necessary? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was ported from the existing View-Based DaxTextInput to support various different states (like having a non-editable, but enabled and clickable input). Taking a second look I agree it's a bit confusing, especially since enabled for example can be overwritten by editable or clickable. Maybe using an enum could be better? I'm open to suggestions
| enum class DaxTextFieldLineLimits { | ||
| /** | ||
| * The TextField will take up a single line and will scroll horizontally if the text is too long. | ||
| */ | ||
| SingleLine, | ||
|
|
||
| /** | ||
| * The TextField will start with a one line height and then expand vertically as needed to accommodate the text. | ||
| */ | ||
| MultiLine, | ||
|
|
||
| /** | ||
| * The TextField will always take at least 3 lines of height and then expand vertically as needed based on the input. | ||
| */ | ||
| Form, | ||
|
|
||
| ; | ||
|
|
||
| /** | ||
| * Converts this [DaxTextFieldLineLimits] to a [TextFieldLineLimits] used by the underlying TextField. | ||
| */ | ||
| fun toLineLimits(): TextFieldLineLimits = when (this) { | ||
| SingleLine -> TextFieldLineLimits.SingleLine | ||
| MultiLine -> TextFieldLineLimits.MultiLine(minHeightInLines = 1) | ||
| Form -> TextFieldLineLimits.MultiLine(minHeightInLines = 3) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another approach with Compose is to define one generic component that will handle for us all the UI customisation and then, propose easy to use variant components for dedicated cases. It depends a lot the experience we want to propose to the team.
| window = colorResource(R.color.white), | ||
| destructive = colorResource(R.color.alertRedOnLightDefault), | ||
| lines = colorResource(R.color.black9), | ||
| borders = colorResource(R.color.black30), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends a lot on the design spec. This borders is used only in the text field or every time we need to use a border, they'll use the exact same border color?
Task/Issue URL: https://app.asana.com/1/137249556945/project/1210594645151737/task/1211659112661196?focus=true
Description
Adds a Compose version of the DaxTextField.
Steps to test this PR
ADS Preview
UI changes
See ADS Preview screen