Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
3.29k stars 2.72k forks source link

[HOLD for Payment 2024-08-22] [$250] Submit expense - Tabs disappear when changing from Scan to Distance via SHIFT+TAB #43719

Closed lanitochka17 closed 2 weeks ago

lanitochka17 commented 2 months ago

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 1.4.83-0 Reproducible in staging?: Y Reproducible in production?: N If this was caught during regression testing, add the test name, ID and link from TestRail: https://expensify.testrail.io/index.php?/tests/view/4626993 Issue reported by: Applause - Internal Team

Action Performed:

  1. Go to staging.new.expensify.com
  2. Go to FAB > Submit expense
  3. Go to Scan
  4. Click on empty area on the RHP (like the empty header area)
  5. Press SHIFT+TAB

Expected Result:

Nothing will happen

Actual Result:

RHP changes to Distance flow, and tabs (Manual, Scan, Distance) are missing

Workaround:

Unknown

Platforms:

Which of our officially supported platforms is this issue occurring on?

Screenshots/Videos

Add any screenshot/video evidence

https://github.com/Expensify/App/assets/78819774/d86b3e9c-7d57-4722-b6ae-547be8e690f1

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~0112bb5e5d8d723e7f
  • Upwork Job ID: 1801340710232481793
  • Last Price Increase: 2024-07-18
  • Automatic offers:
    • rayane-djouah | Reviewer | 103221643
    • dominictb | Contributor | 103221644
Issue OwnerCurrent Issue Owner: @rayane-djouah
melvin-bot[bot] commented 2 months ago

πŸ“£ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? πŸ’Έ

OfstadC commented 1 month ago

@rayane-djouah Sorry for the delay here as I was OoO. I've added this to Wave Collect and will update once we have a priority there.

OfstadC commented 1 month ago

Cool it Melvin πŸ˜…

melvin-bot[bot] commented 1 month ago

Auto-assigning issues to engineers is no longer supported. If you think this issue should receive engineering attention, please raise it in #whatsnext.

trjExpensify commented 1 month ago

^^ Dammit, the sidepane jumped as I was trying to click the project. πŸ˜‚

rayane-djouah commented 1 month ago

Thank you for the update

melvin-bot[bot] commented 1 month ago

πŸ“£ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? πŸ’Έ

OfstadC commented 1 month ago

I imagine a "Polish" doesn't need to be increased, but curious your thoughts @trjExpensify πŸ˜„

melvin-bot[bot] commented 1 month ago

@tgolen @OfstadC @rayane-djouah this issue is now 4 weeks old, please consider:

Thanks!

rayane-djouah commented 1 month ago

Started a discussion in #expensify-open-source

trjExpensify commented 1 month ago

I imagine a "Polish" doesn't need to be increased, but curious your thoughts @trjExpensify πŸ˜„

Agree.

dominictb commented 1 month ago

Proposal

Please re-state the problem that we are trying to solve in this issue.

Tabs disappear when changing from Scan to Distance via SHIFT+TAB

What is the root cause of that problem?

The OnyxTabNavigator will render all 3 tab screens, but the focused tab screen will be visible, and other tab screen will be moved to the left/right depending on the position, being hidden to the user

image

In IOURequestStartPage, we are wrapping a ScreenWrapper with focus trap enabled. Now based on how the focus-trap library works, the following happens:

What changes do you think we should make in order to solve the problem?

We have 3 options:

<ScreenWrapper focusTrapOptionsPassThrough={{ preventScroll: false }} />

this will prevents the focus node to scroll into view if it is out of view, making UI shift like described.

type Props {
  onlyRenderWhenFocused?: boolean
}

....
const isFocused = useIsFocused()

if(!isFocused && onlyRenderWhenFocused) return null;

And in here, add onlyRenderWhenFocused=true for 3 components.

What alternative solutions did you explore? (Optional)

Same idea with option 3 above, but we can initiate the focus trap so that only elements under the active tab are added to the focus trap accordingly.

Detailed solution

We can utilize the `containerElements` prop of the focus-trap-react. Check [here](https://github.com/focus-trap/focus-trap-react?tab=readme-ov-file#containerelements). The idea is that we should get the root `View`s of each tab component `IOURequestStepAmount`, `IOURequestStepScan`, `IOURequestStepDistance`, then based on the `selectedTab` to register the `View` as `containerElements` of the focus trap. We should also consider the HeaderWithBackButton and the TabSelector component to be one of the containers. We can achieve that by following change: - Define a `useFocusTrapContainers` hook ``` import { useCallback, useState } from "react"; import { UseFocusTrapContainers } from "./type"; const useFocusTrapContainers: UseFocusTrapContainers = () => { const [containers, setContainers] = useState([]); const addContainer = useCallback((container: HTMLElement) => { const removeContainer = () => setContainers((prevContainers) => prevContainers.filter((c) => c !== container)); setContainers((prevContainers) => prevContainers.includes(container) ? prevContainers : [...prevContainers, container]); return removeContainer; }, []) return [containers, addContainer] } export default useFocusTrapContainers; ``` Note: It is important we allow the container to be removed if the respective component is unmounted. We will call the hooks 3 times in `IOURequestStartPage` to create 3 functions to register container for each tab. We can have 1 more hook call to have a function to register containers for the other components like `HeaderWithBackButton` and `TabSelector` ``` const [manualTabContainers, addManualTabContainer] = useFocusTrapContainers(); const [scanTabContainers, addScanTabContainer] = useFocusTrapContainers(); const [distanceTabContainers, addDistanceTabContainer] = useFocusTrapContainers(); ``` Now for each tab component, like `IOURequestTabManual`, we need to drill down the prop `registerFocusTrapContainer=addManualTabContainer` down the tree, until we meet the first root `View`. For `IOURequestTabManual`, the prop needs to pass down from `IOURequestTabManual` > `MoneyRequestAmountForm` > [`ScrollView`](https://github.com/Expensify/App/blob/f392cd369131e80b09e1ada8afd239d461257b24/src/pages/iou/MoneyRequestAmountForm.tsx#L254). And in that `ScrollView`, we'll need to provide a ref callback to register that View as focus trap container ```typescript { if(scrollViewNode) { const unregister = props.registerFocusTrapContainer?.(scrollViewNode as unknown as HTMLElement); return () => unregister?.(); } }} contentContainerStyle={styles.flexGrow1}> ``` We should drill the props for `IOURequestStepScan` and `IOURequestStepDistance` as well. Finally, we should calculate the list of container elements in `IOURequestStartPage`: ``` const focusTrapContainers = [ ...(selectedTab === CONST.TAB_REQUEST.MANUAL ? manualTabContainers : []), ...(selectedTab === CONST.TAB_REQUEST.SCAN ? scanTabContainers : []), ...(selectedTab === CONST.TAB_REQUEST.DISTANCE ? distanceTabContainers : []), ] ``` And then pass down to `ScreenWrapper` > `FocusTrapForScreen` to initialize the focus trap. In order to avoid the prop drilling we can try to use React Context instead (note that we have to avoid the case: if a `View` is registered as a focus trap container, all components in the child subtree cannot be registered to avoid redundancy), but that's more of a generic solution and I would love to have the team's input before working on this.

melvin-bot[bot] commented 1 month ago

@tgolen, @OfstadC, @rayane-djouah Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

rayane-djouah commented 1 month ago

@dominictb - Thank you for explaining the RCA in such detail! It makes sense to me and is the most clearly explained so far. I think the most accurate solution for this bug, based on what you suggested, is what you described here (if we want to keep tab navigation disabled on this page):

  • We can add a props focusTrapOptionsPassthrough in ScreenWrapper, and in IOURequestStartPage, set
<ScreenWrapper focusTrapOptionsPassThrough={{ preventScroll: true }} /> // I think you've made a typo here (should be true instead of false)

this will prevents the focus node to scroll into view if it is out of view, making UI shift like described.

However, I still want to explore solutions that exclude focusable elements from inactive tabs to re-enable proper tab navigation on this page. The most relevant solution for now involves using the CSS visibility property, which I think is not optimal (kind of a workaround). Is there a possibility to use focus trap properties for this instead? Have you explored other solutions in this context?

dominictb commented 1 month ago

@rayane-djouah I'll explore it today and let you know!

dominictb commented 1 month ago

I think I figured out the way to do this, but I'll need some time to test this out. Will get back by tomorrow at most.

vishnu-karuppusamy commented 1 month ago

https://github.com/Expensify/App/issues/43719#issuecomment-2168110266

@rayane-djouah Updated the below sections:

What changes do you think we should make in order to solve the problem? What alternative solutions did you explore? (Optional)

dominictb commented 1 month ago

Is there a possibility to use focus trap properties for this instead?

I'll update this first thing tomorrow.

dominictb commented 1 month ago

Is there a possibility to use focus trap properties for this instead? Have you explored other solutions in this context? @rayane-djouah

We can utilize the containerElements prop of the focus-trap-react. Check here. The idea is that we should get the root Views of each tab component IOURequestStepAmount, IOURequestStepScan, IOURequestStepDistance, then based on the selectedTab to register the View as containerElements of the focus trap. We should also consider the HeaderWithBackButton and the TabSelector component to be one of the containers.

We can achieve that by following change:

const useFocusTrapContainers: UseFocusTrapContainers = () => { const [containers, setContainers] = useState<HTMLElement[]>([]);

const addContainer = useCallback((container: HTMLElement) => { const removeContainer = () => setContainers((prevContainers) => prevContainers.filter((c) => c !== container)); setContainers((prevContainers) => prevContainers.includes(container) ? prevContainers : [...prevContainers, container]);

return removeContainer;

}, [])

return [containers, addContainer] }

export default useFocusTrapContainers;

Note: It is important we allow the container to be removed if the respective component is unmounted.

We will call the hooks 3 times in `IOURequestStartPage` to create 3 functions to register container for each tab. We can have 1 more hook call to have a function to register containers for the other components like `HeaderWithBackButton` and `TabSelector`

const [manualTabContainers, addManualTabContainer] = useFocusTrapContainers(); const [scanTabContainers, addScanTabContainer] = useFocusTrapContainers(); const [distanceTabContainers, addDistanceTabContainer] = useFocusTrapContainers();


Now for each tab component, like `IOURequestTabManual`, we need to drill down the prop `registerFocusTrapContainer=addManualTabContainer` down the tree, until we meet the first root `View`. For `IOURequestTabManual`, the prop needs to pass down from `IOURequestTabManual` > `MoneyRequestAmountForm` > [`ScrollView`](https://github.com/Expensify/App/blob/f392cd369131e80b09e1ada8afd239d461257b24/src/pages/iou/MoneyRequestAmountForm.tsx#L254). And in that `ScrollView`, we'll need to provide a ref callback to register that View as focus trap container

```typescript
<ScrollView ref={(scrollViewNode) => {
            if(scrollViewNode) {
                const unregister = props.registerFocusTrapContainer?.(scrollViewNode as unknown as HTMLElement);
                return () => unregister?.();
            }
        }} contentContainerStyle={styles.flexGrow1}>

We should drill the props for IOURequestStepScan and IOURequestStepDistance as well.

Finally, we should calculate the list of container elements in IOURequestStartPage:

const focusTrapContainers = [
        ...(selectedTab === CONST.TAB_REQUEST.MANUAL ? manualTabContainers : []),
        ...(selectedTab === CONST.TAB_REQUEST.SCAN ? scanTabContainers : []),
        ...(selectedTab === CONST.TAB_REQUEST.DISTANCE ? distanceTabContainers : []),
    ]

And then pass down to ScreenWrapper > FocusTrapForScreen to initialize the focus trap.

In order to avoid the prop drilling we can try to use React Context instead (note that we have to avoid the case: if a View is registered as a focus trap container, all components in the child subtree cannot be registered to avoid redundancy), but that's more of a generic solution and I would love to have the team's input before working on this.

dominictb commented 1 month ago

@rayane-djouah for a simpler solution, you can simply don't render anything if the tab isn't focus. Check out option 3 of my original proposal. We can even lift the logic up to IOURequestStepScan

<Tab.Screen>
   {() => {
      if(selectedTab !== 'scan') return null
     return <IOURequestStepScan .../>
   }}
</Tab.Screen>
rayane-djouah commented 1 month ago

Reviewing, will update today

OfstadC commented 1 month ago

Good timing @rayane-djouah - I was just typing a bump πŸ˜… - Thanks you for updating! πŸ˜ƒ

melvin-bot[bot] commented 1 month ago

πŸ“£ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? πŸ’Έ

rayane-djouah commented 1 month ago

@dominictb - Can you please share a test branch of this solution? also, please include the solution in your original proposal.

mvtglobally commented 1 month ago

Issue not reproducible during KI retests. (First week)

dominictb commented 1 month ago

@rayane-djouah https://github.com/Expensify/App/compare/main...dominictb:epsf-app:fix/43719-shift-tab-test

This roughly the idea. The detailed implementation can be refined later.

dominictb commented 1 month ago

Proposal updated.

rayane-djouah commented 1 month ago

@rayane-djouah for a simpler solution, you can simply don't render anything if the tab isn't focus. Check out option 3 of my original proposal. We can even lift the logic up to IOURequestStepScan

<Tab.Screen>
   {() => {
      if(selectedTab !== 'scan') return null
     return <IOURequestStepScan .../>
   }}
</Tab.Screen>

As I mentioned here: https://github.com/Expensify/App/issues/43719#issuecomment-2178614112 This solution will cause a performance issue (re-render delays when tab switching, especially for the map in the distance tab)

rayane-djouah commented 1 month ago

@tgolen, Summary: Tab navigation was disabled on the initial page of the IOU flow as per this PR to address this issue. Subsequently, the implementation of a focus trap in this PR caused this regression, as explained in the RCA within this proposal. To fix this bug from root, it is recommended to set the preventScroll option to true as outlined in the second option of the solution section in the proposal. If re-enabling tab navigation on this page is desired, we can utilize the containerElements prop from focus-trap-react, as suggested here in the alternative solution section of the proposal. I defer the decision to you on whether we should re-enable tab navigation on this page, especially considering our recent focus on improving tab navigation in the app, as seen in https://github.com/Expensify/App/issues/36476.

@dominictb's proposal looks good to me.

:ribbon::eyes::ribbon: C+ reviewed

melvin-bot[bot] commented 1 month ago

Current assignee @tgolen is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

vishnu-karuppusamy commented 1 month ago

I would like to re-assist the solutions proposed as both the solutions proposed such as hiding the in-active tabs and the use of containerElements prop in the focus-trap-react were proposed initially by me.

@rayane-djouah could you please confirm the above statement?

cc: @OfstadC

tgolen commented 1 month ago

I think we should try to have tab navigation anywhere we can, so if it's possible to re-enable tab navigation (it sounds like it), then I think we should do it. I'll wait for ^ question to be resolved before doing anything here though.

On Sun, Jul 21, 2024 at 2:34β€―AM vishnu-karuppusamy @.***> wrote:

I would like to re-assist the solutions proposed as both the solutions proposed such as hiding the in-active tabs and the use of containerElements prop in the focus-trap-react were proposed initially by me.

@rayane-djouah https://github.com/rayane-djouah could you please confirm the above statement?

cc: @OfstadC https://github.com/OfstadC

β€” Reply to this email directly, view it on GitHub https://github.com/Expensify/App/issues/43719#issuecomment-2241527627, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJMABZ5WK76ZMN3YCTKTDLZNNXAJAVCNFSM6AAAAABJI5OZFOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENBRGUZDONRSG4 . You are receiving this because you were mentioned.Message ID: @.***>

rayane-djouah commented 1 month ago

I would like to re-assist the solutions proposed as both the solutions proposed such as hiding the in-active tabs and the use of containerElements prop in the focus-trap-react were proposed initially by me.

@rayane-djouah could you please confirm the above statement?

@vishnu-karuppusamy - Thank you for your initial suggestion regarding the use of the containerElements prop. I appreciate your contribution to this discussion. I noticed, however, that your proposal might benefit from a more detailed explanation, particularly concerning the root cause analysis of this bug and a fully elaborated working solution.

I tried implementing the solution you suggested, as seen in the test branch, but unfortunately, it did not resolve the bug. In striving for the clearest and highest-quality proposal, with a fully working solution, I've decided to proceed with @dominictb's proposal that meets these criteria more fully.

Thank you again for your efforts and understanding.

rayane-djouah commented 1 month ago

@tgolen - Thank you for confirming the way to move forward! I agree that enabling tab navigation wherever possible will improve usability. Let's go ahead with @dominictb's solution.

melvin-bot[bot] commented 1 month ago

πŸ“£ @rayane-djouah πŸŽ‰ An offer has been automatically sent to your Upwork account for the Reviewer role πŸŽ‰ Thanks for contributing to the Expensify app!

Offer link Upwork job

melvin-bot[bot] commented 1 month ago

πŸ“£ @dominictb πŸŽ‰ An offer has been automatically sent to your Upwork account for the Contributor role πŸŽ‰ Thanks for contributing to the Expensify app!

Offer link Upwork job Please accept the offer and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review πŸ§‘β€πŸ’» Keep in mind: Code of Conduct | Contributing πŸ“–

OfstadC commented 1 month ago

Any update here @dominictb @rayane-djouah ? πŸ˜ƒ

rayane-djouah commented 1 month ago

PR under review

melvin-bot[bot] commented 3 weeks ago

⚠️ Looks like this issue was linked to a Deploy Blocker here

If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

If a regression has occurred and you are the assigned CM follow the instructions here.

If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

rayane-djouah commented 2 weeks ago

This ^ is a false positive https://github.com/Expensify/App/issues/47381#issuecomment-2288264744

rayane-djouah commented 2 weeks ago

[!NOTE] The production deploy automation failed: This should be on [HOLD for Payment 2024-08-22] according to https://github.com/Expensify/App/issues/47356 prod deploy checklist, confirmed in https://github.com/Expensify/App/pull/45982#issuecomment-2294129588.

cc @OfstadC

OfstadC commented 2 weeks ago

Thanks @rayane-djouah - since the automation failed. I'll posted the checklist below πŸ˜„

rayane-djouah commented 2 weeks ago

Checklist

Regression Test Proposal

  1. Open the app on web / desktop
  2. Go to FAB > Submit expense
  3. Verify that tab navigation works correctly
  4. Verify that the focus jumps around the page but there's no UI shifting (Page tabs should not disappear)
  5. Click on empty area on the RHP (like the empty header area)
  6. Press SHIFT+TAB
  7. Verify that there's no UI shifting (Page tabs should not disappear)

Do we agree πŸ‘ or πŸ‘Ž

cc @OfstadC

OfstadC commented 2 weeks ago

Payment Summary: