-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Add additional events for controlling picker indicator on web #15673
Conversation
@marcaaron @thesahindia One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
Hey! I see that you made changes to our Form component. Make sure to update the docs in FORMS.md accordingly. Cheers! |
@redstar504, the picker stays highlighted when we close it without changing the value Screen.Recording.2023-03-06.at.11.36.13.PM.movI think we should handle this case as well. cc: @marcaaron for thoughts |
src/components/Picker.js
Outdated
const index = e.target.selectedIndex; | ||
const value = e.target.options[index].value; | ||
this.onInputChange(value, index); | ||
this.setState({isOpen: 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.
It feels like we are sneaking in some platform dependent code here, but it's not clear which is the web or native implementation. Normally, how we deal with that is create a "base" component and then an index.js
and index.native.js
components. In a file like Picker.js
we'd then just import the Picker but the platform dependent changes would be contained in the respective versions of the picker. Would this approach work here?
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.
Yeah that would be better. Also let's create a function in index.js file and call it in onChange
(just to make this a little bit cleaner)
@thesahindia I don't feel super strongly about solving that case in this ticket. We didn't spot it until now and it's not clear what the solution would be - so I say we continue here and then file it as a separate bug. Unless @redstar504 has some idea on how to tackle it and is willing to look into it. |
That makes sense to me! |
@marcaaron @thesahindia Thanks for the input guys. I have been exploring ways to split up the platform-specific code as requested. It appears that our goal is to share the Picker component across all platforms while providing a means to specify any platform-specific event handlers through their respective index files. Would it make sense to refactor |
Yes! |
My latest commit includes platform specific event handling for the Picker component. The implementation involves wrapping the refactored BasePicker with platform specific entrypoint files, and wrapping the entrypoints with a higher order component that supplies default events for both platforms. Web specific events are encapsulated in the index.js file. Native events are supplied as defaults through the HOC since they supplement the web-based events. Testing on all platforms was successful, and I have included new recordings in my checklist. |
src/components/withPickerEvents.js
Outdated
import {pickerPropTypes, pickerDefaultProps} from './Picker/pickerPropTypes'; | ||
|
||
export default function (WrappedComponent) { | ||
class WithSharedEvents extends Component { |
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 haven't delved deeply into the reasons for needing it, but is an HOC necessary? Feels like the "WrappedComponent" is always going to be the Picker
.
Additionally, I'm having trouble following some of the conventions used here e.g. the callbackConsumer()
(stuck out to me as unusual for our codebase and my instincts are telling me it could confuse people).
I'd have to do a deeper dive to come up with more concrete suggestions, but just wanted to toss that observation out there that maybe we could simplify 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.
The HOC wraps the Picker in both entrypoints (index.js and index.native.js), so that we can share all of the code contained in it between them. This also gives us a way to manipulate the events passed down to BasePicker using an entrypoint file. I originally had placed the HOC in the Picker directory because it would only ever be used for the Picker, but I noticed all of the others were contained in the base src
directory so I moved it there.
An HOC would seem like overkill but I am kind of stuck between a dead simple solution (the one I originally provided), and an over-engineered solution. I have been having trouble finding something in the middle ground that won't introduce a lot of code duplication. Another option might be class inheritance.
We basically need a way to share all of the event handlers between both entrypoints but give the web entry point a way to extend/override them before passing them down to BasePicker. If you have any alternative ideas for that please let me know. If we stick with the HOC I can definitely rename callbackConsumer
back to onInputChange
.
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.
An HOC would seem like overkill but I am kind of stuck between a dead simple solution (the one I originally provided), and an over-engineered solution. I have been having trouble finding something in the middle ground that won't introduce a lot of code duplication. Another option might be class inheritance.
Yeah I agree the original solution was simple, this feels a little over-engineered now. I am going to defer to @marcaaron on 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.
Ok I see the issue now - sorry for the distraction here. I think where this is at is a bit hard to reason about for not much gain.
Here's what I'm thinking as an alternative...
- Start by reverting what you have here back to 15fb9c7
- Create some prop like
shouldUseAdditionalPickerEvents
on the Picker - Override the methods that way inside the Picker
It might seem vain, but we can get around the cross platform file extension convention this way and it will allow web/native to switch on/off the functionality we need without specifically referencing the platform anywhere.
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.
@marcaaron Thank you for the advice. I just need a bit more clarification:
It might seem vain, but we can get around the cross platform file extension convention this way and it will allow web/native to switch on/off the functionality we need without specifically referencing the platform anywhere.
Are you suggesting not having platform-specific entry points in this case? I am just curious what is going to toggle shouldUseAdditionalPickerEvents
if we don't, and if we didn't, where do we define the web-specific events?
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.
Sorry, I meant something like...
/Picker/index.js -> <Picker shouldUseAdditionalPickerEvents />
/Picker/index.native.js -> <Picker />
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.
@marcaaron Ok I will give that a shot. Since it's just a conditional in the entry points, is this implying that we keep the web events defined in the Picker itself? Just want to clarify exactly what we're after to avoid further delays.
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.
implying that we keep the web events defined in the Picker itself
That was my meaning yea.
But I thought of something that is slightly better and might result in fewer changes while avoiding a reference to the web specific e.target
...
- Create
getAdditionalPickerEvents()
- have native return
{}
- have web return something like
- have native return
// getAdditionalPickerEvents/index.js
function getAdditionalPickerEvents(onMouseDown, onChange) {
return {
onMouseDown,
onChange: (e) = {
if (e.target.selectedIndex === undefined) {
return;
}
const index = e.target.selectedIndex;
const value = e.target.options[index].value;
onChange(value, index);
},
};
}
Then pass them like:
pickerProps={{
...getAdditionalPickerEvents(this.setIsOpen, this.updateAndClose),
This feels slightly better to me as it manages the web event outside of the cross platform Picker code. Sorry for all the back and forth. Our style guide sometimes paints us into a corner 😄
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.
@marcaaron I like the looks of that. I have been trying to find a way to pass down the events from the web entry, but also to be able to pass a function back that can enable/disable the highlight without having to reference them from inside the shared Picker. This looks like it could work.
Also, no worries. I am happy to work towards something we can all be satisfied with.
40c36bc
to
6e3b8fc
Compare
That concept worked perfectly @marcaaron, nice job. The way I implemented it is by passing the I refactored all of the Again, checks out on all platforms. |
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.
Looks great to me. Just a couple of places where I think we could use a bound function instead of an anonymous arrow function.
@marcaaron Requested changes pushed! |
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.
Didn't test but code LGTM so approving. All yours @thesahindia.
src/components/Picker/Picker.js
Outdated
setIsOpen() { | ||
this.setState({ | ||
isOpen: true, | ||
}); | ||
} | ||
|
||
setIsClosed() { | ||
this.setState({ | ||
isOpen: 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.
The names here sounds confusing. When I looked at them I felt that we have two different state (isClosed
and isOpen
) I think we should change the names for these methods or we can just use one method (setIsOpen
) and pass true/false as argument.
What do you guys 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.
You are right, I think we should clarify what that state variable actually controls. Since it is only responsible for enabling and disabling the highlight, we can make it more understandable by using an isHighlighted
state variable, and calling enableHighlight
and disableHighlight
to control it.
import Text from '../Text'; | ||
import styles from '../../styles/styles'; | ||
import themeColors from '../../styles/themes/default'; | ||
import {ScrollContext} from '../ScrollViewWithContext'; | ||
|
||
const propTypes = { |
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.
Let's create a new file pickerPropTypes.js
and define all the props there except additionalPickerEvents
. Let's import the propTypes and defaultProps from pickerPropTypes.js
and use them at native and index.js too. Here's an example.
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.
Since no props are specifically referenced in the platform-specific wrappers, it might be unnecessary to perform validation there before passing them to the BasePicker. However, if this is an established pattern in the codebase, it would be a good idea to follow that with our changes as well.
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 looks like we are doing it but I am not sure if it's needed. I think @marcaaron would know more about it or we can ask at expensify-open-source
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 think I don't feel too passionate about this one. Or at least I can't think of a situation where it would help because the props are just getting passed straight to the BasePicker. If there was some prop validation warning we would see it either way.
Thanks for the review @thesahindia. I have added your requested changes to the latest commit. |
Reviewer Checklist
Screenshots/VideosWebScreen.Recording.2023-03-14.at.12.41.51.AM.movMobile Web - ChromeScreen.Recording.2023-03-14.at.1.21.04.AM.movMobile Web - SafariScreen.Recording.2023-03-14.at.1.23.21.AM.movDesktopScreen.Recording.2023-03-14.at.12.41.04.AM.moviOSScreen.Recording.2023-03-14.at.1.25.37.AM.movAndroidScreen.Recording.2023-03-14.at.1.10.13.AM.mov |
The error is due to the prop validation we've added in the entrypoints. Adding For example, this works but does not provide any benefit: // index.js
import {defaultProps, propTypes} from './pickerPropTypes';
// eslint-disable-next-line react/jsx-props-no-spreading
const Picker = props => <BasePicker {...props} additionalPickerEvents={additionalPickerEvents} />;
Picker.propTypes = propTypes;
Picker.defaultProps = defaultProps;
// eslint-disable-next-line react/jsx-props-no-spreading
export default forwardRef((props, ref) => <Picker {...props} innerRef={ref} />); Using shared I suggest that we rollback this change as it is not necessary for our scenario. |
@thesahindia I reverted shared prop types due to the reasons described in my last comment. Props are still validated as expected through the base Picker component, and there are no console errors. So we should be good. |
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 take back what I said. It didn't test well. I was checking other pages to confirm that all the functionalities are working as expected. It is not working correctly. Not getting the error message for the picker. @redstar504, can you look into it?
I am on a slow internet so I couldn't upload the video. You can try reproducing it at "Add debit card page" or any other page where we are validating.
@thesahindia Great job catching that. We needed to pass the |
ebd3673
to
2924efb
Compare
2924efb
to
f956e52
Compare
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.
Thanks! It works well.
cc: @marcaaron
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.
Nice job here overall and LGTM. Thanks @thesahindia for the comments and testing 🙇
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
🚀 Deployed to staging by https://github.com/marcaaron in version: 1.2.85-0 🚀
|
🚀 Deployed to production by https://github.com/Julesssss in version: 1.2.85-1 🚀
|
Details
I had to work through this a bit more to make the picker indicator behave consistently all platforms. As proposed, I added two new event handlers specific to the web version of the app that update the
isOpen
state, but they needed to be adapted a bit from what I suggested.The original proposal worked to solve the reported issue, but it was causing the picker to remain highlighted on desktop web after selecting an item. To solve this, I switched from using
onClick
toonMouseDown
. This solved it simply because of the order the event handlers are called in.In addition, I included a new
onChange
event handler whose presence overrides theonValueChange
handler for web. This was necessary because my original solution of settingisOpen
tofalse
in theonValueChange
event was resulting in the indicator changing back to the default setting whenever a new item was scrolled to in the iOS picker wheel.Fixed Issues
$ #15506
PROPOSAL: #15506 (comment)
Tests
Settings > Workspace > Reimburse Expenses
picker for selecting mileage units.Offline tests
QA Steps
Settings > Workspace > Reimburse Expenses
picker for selecting mileage units.PR Author Checklist
### Fixed Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)src/languages/*
files and using the translation methodWaiting for Copy
label for a copy review on the original GH to get the correct copy.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)ScrollView
component to make it scrollable when more elements are added to the page.main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.Screenshots/Videos
Web
Safari
border-mac-safari.mp4
Chrome
Having an issue with OBS not displaying
<select>
elements during a windowed screen recording (seems likeselect
elements are considered a separate window), but still demonstrates the functionality:pf-desktop-web.mp4
Mobile Web - Chrome
pf-android-chrome.mp4
Mobile Web - Safari
border-ios-safari.mp4
Desktop
pf-mac-desktop.mp4
iOS
pf-ios-native.mp4
Android
pf-android-native.mp4