Skip to content
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

[$250] Submit expense - Tabs disappear when changing from Scan to Distance via SHIFT+TAB #43719

Open
2 of 6 tasks
lanitochka17 opened this issue Jun 13, 2024 · 65 comments
Open
2 of 6 tasks
Assignees
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2 Engineering External Added to denote the issue can be worked on by a contributor Help Wanted Apply this label when an issue is open to proposals by contributors

Comments

@lanitochka17
Copy link

lanitochka17 commented Jun 13, 2024

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?

  • Android: Native
  • Android: mWeb Chrome
  • iOS: Native
  • iOS: mWeb Safari
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Add any screenshot/video evidence

Bug6512234_1718299693563.20240614_012437.mp4

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-11
Issue OwnerCurrent Issue Owner: @rayane-djouah
@lanitochka17 lanitochka17 added DeployBlockerCash This issue or pull request should block deployment DeployBlocker Indicates it should block deploying the API labels Jun 13, 2024
Copy link

melvin-bot bot commented Jun 13, 2024

Triggered auto assignment to @tgolen (DeployBlockerCash), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.

Copy link
Contributor

👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:

  1. Identify the pull request that introduced this issue and revert it.
  2. Find someone who can quickly fix the issue.
  3. Fix the issue yourself.

@lanitochka17
Copy link
Author

@tgolen FYI I haven't added the External label as I wasn't 100% sure about this issue. Please take a look and add the label if you agree it's a bug and can be handled by external contributors

@lanitochka17
Copy link
Author

We think that this bug might be related to #vip-vsp

@tgolen tgolen added Daily KSv2 External Added to denote the issue can be worked on by a contributor Bug Something is broken. Auto assigns a BugZero manager. and removed DeployBlockerCash This issue or pull request should block deployment Hourly KSv2 DeployBlocker Indicates it should block deploying the API labels Jun 13, 2024
@melvin-bot melvin-bot bot changed the title Submit expense - Tabs disappear when changing from Scan to Distance via SHIFT+TAB [$250] Submit expense - Tabs disappear when changing from Scan to Distance via SHIFT+TAB Jun 13, 2024
Copy link

melvin-bot bot commented Jun 13, 2024

Job added to Upwork: https://www.upwork.com/jobs/~0112bb5e5d8d723e7f

@melvin-bot melvin-bot bot added the Help Wanted Apply this label when an issue is open to proposals by contributors label Jun 13, 2024
Copy link

melvin-bot bot commented Jun 13, 2024

Triggered auto assignment to @OfstadC (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

Copy link

melvin-bot bot commented Jun 13, 2024

Triggered auto assignment to Contributor-plus team member for initial proposal review - @rayane-djouah (External)

@tgolen
Copy link
Contributor

tgolen commented Jun 13, 2024

I'm demoting this one and assigning to external.

@tsa321
Copy link
Contributor

tsa321 commented Jun 13, 2024

cannot reproduce in production release. Offending PR: #39520

@studentofcoding
Copy link
Contributor

Not produceable on Production and local

@vishnu-karuppusamy
Copy link

vishnu-karuppusamy commented Jun 14, 2024

Proposal

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

Pressing SHIFT+TAB on the top section of the RHP updates the contents of the RHP.

What is the root cause of that problem?

  • The current implementation has a common FocusTrap wrapper (ScreenWrapper) for all three screens (Manual, Scan & Distance).
{* Common wrapper for all the three screens *}
<ScreenWrapper
    includeSafeAreaPaddingBottom={false}
    shouldEnableKeyboardAvoidingView={false}
    shouldEnableMinHeight={DeviceCapabilities.canUseTouchScreen()}
    headerGapStyles={isDraggingOver ? [styles.receiptDropHeaderGap] : []}
    testID={IOURequestStartPage.displayName}
>
    {({safeAreaPaddingBottomStyle}) => (
        <DragAndDropProvider
            setIsDraggingOver={setIsDraggingOver}
            isDisabled={selectedTab !== CONST.TAB_REQUEST.SCAN}
        >
            <View style={[styles.flex1, safeAreaPaddingBottomStyle]}>
                <HeaderWithBackButton
                    title={tabTitles[iouType]}
                    onBackButtonPress={navigateBack}
                />
                {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY && iouType !== CONST.IOU.TYPE.INVOICE ? (
                    <OnyxTabNavigator
                        id={CONST.TAB.IOU_REQUEST_TYPE}
                        onTabSelected={resetIOUTypeIfChanged}
                        tabBar={TabSelector}
                    >
                        <TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>
                            {() => (
                                <IOURequestStepAmount
                                    shouldKeepUserInput
                                    route={route}
                                />
                            )}
                        </TopTab.Screen>
                        <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen>
                        {shouldDisplayDistanceRequest && <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>{() => <IOURequestStepDistance route={route} />}</TopTab.Screen>}
                    </OnyxTabNavigator>
                ) : (
                    <IOURequestStepAmount route={route} />
                )}
            </View>
        </DragAndDropProvider>
    )}
</ScreenWrapper>
  • When a click is made on the top empty area on the RHP (like the empty header area), the focus gets initiated on the common FocusTrap wrapper. Pressing SHIFT+TAB focuses the last element on the DOM tree which is the Next button of the Distance screen. (In the issue repro video, we can observe that the Next button of the Distance screen is focused)
    image

  • Because of the translating nature of the Tabs component OnyxTabNavigator, the tabs (Manual, Scan, Distance) are translated to the left by 750px when the Next button of the Distance screen is focused. This is the reason behind missing of the tabs.

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

  • Since the TAB navigation focus is already handled by focus-trap library, we can remove the below code block in src/pages/iou/request/IOURequestStartPage.tsx.
useFocusEffect(
    useCallback(() => {
        const handler = (event: KeyboardEvent) => {
            if (event.code !== CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) {
                return;
            }
            event.preventDefault();
            event.stopPropagation();
        };
        KeyDownPressListener.addKeyDownPressListener(handler);

        return () => KeyDownPressListener.removeKeyDownPressListener(handler);
    }, []),
);
  • Since each screen is individually wrapped by FocusTrap by the component ScreenWrapper, we can remove the common ScreenWrapper for all the screens. By this approach, only the active tab's FocusTrap will be active at once.
    FOR PROPOSAL PURPOSES, ONLY A POC-LEVEL CODE IS ADDED BELOW. WILL ADD THE STANDARDIZED CODE WHEN THE SOLUTION IS ACCEPTED.

Update the below in src/pages/iou/request/IOURequestStartPage.tsx:

<AccessOrNotFoundWrapper
    reportID={reportID}
    iouType={iouType}
    policyID={policy?.id}
    accessVariants={[CONST.IOU.ACCESS_VARIANTS.CREATE]}
    allPolicies={allPolicies}
>
    <DragAndDropProvider
        setIsDraggingOver={setIsDraggingOver}
        isDisabled={selectedTab !== CONST.TAB_REQUEST.SCAN}
    >
        <View style={[styles.flex1]}>
            <View id={`${CONST.TAB.IOU_REQUEST_TYPE}_header`}>
                <HeaderWithBackButton
                    title={tabTitles[iouType]}
                    onBackButtonPress={navigateBack}
                />
            </View>
            {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY && iouType !== CONST.IOU.TYPE.INVOICE ? (
                <OnyxTabNavigator
                    id={CONST.TAB.IOU_REQUEST_TYPE}
                    onTabSelected={resetIOUTypeIfChanged}
                    tabBar={(props) => (
                        <TabSelector
                            {...props}
                            id={`${CONST.TAB.IOU_REQUEST_TYPE}_tabsHeader`}
                        />
                    )}
                >
                    <TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>
                        {() => (
                            <View
                                id={CONST.TAB_REQUEST.MANUAL}
                                style={{flex: 1}}
                            >
                                <IOURequestStepAmount
                                    shouldKeepUserInput
                                    route={route}
                                />
                            </View>
                        )}
                    </TopTab.Screen>
                    <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>
                        {() => (
                            <View
                                id={CONST.TAB_REQUEST.SCAN}
                                style={{flex: 1}}
                            >
                                <IOURequestStepScan route={route} />
                            </View>
                        )}
                    </TopTab.Screen>
                    {shouldDisplayDistanceRequest && (
                        <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>
                            {() => (
                                <View
                                    id={CONST.TAB_REQUEST.DISTANCE}
                                    style={{flex: 1}}
                                >
                                    <IOURequestStepDistance route={route} />
                                </View>
                            )}
                        </TopTab.Screen>
                    )}
                </OnyxTabNavigator>
            ) : (
                <IOURequestStepAmount route={route} />
            )}
        </View>
    </DragAndDropProvider>
</AccessOrNotFoundWrapper>

Update the below in src/components/FocusTrap/FocusTrapForScreen/index.web.tsx:

Line no 10: import CONST from '@src/CONST';
Line no: 65:
containerElements={
    isActive && !!TOP_TAB_SCREENS.find((screen) => screen === route.name)
        ? [
                document.getElementById(`${CONST.TAB.IOU_REQUEST_TYPE}_header`),
                document.getElementById(`${CONST.TAB.IOU_REQUEST_TYPE}_tabsHeader`),
                document.getElementById(route.name),
            ]
        : undefined
}

Update the below in src/components/TabSelector/TabSelector.tsx:

Line no 17: id?: string;
Line no 57: function TabSelector({state, navigation, onTabPress = () => {}, position, id}: TabSelectorProps) {
Line no 87: 
<View
    style={styles.tabSelector}
    id={id}
>

Update the below in src/pages/iou/request/step/IOURequestStepAmount.tsx:

Line no 314: 
// shouldShowWrapper={!!backTo || isEditing}
shouldShowWrapper
hideHeader

Update the below in src/pages/iou/request/step/IOURequestStepDistance.tsx:

Line no 314: 
// shouldShowWrapper={!isCreatingNewRequest}
shouldShowWrapper
hideHeader

Update the below in src/pages/iou/request/step/IOURequestStepScan/index.tsx:

Line no 314: 
// shouldShowWrapper={!!backTo}
shouldShowWrapper
hideHeader

Update the below in src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx:

Line no 29: 
/** Hide the header when the wrapper is shown */
hideHeader?: boolean;
Line no 34:
function StepScreenDragAndDropWrapper({testID, hideHeader, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper}: StepScreenDragAndDropWrapperProps) {
Line no 55:
{!hideHeader ? (
    <HeaderWithBackButton
        title={headerTitle}
        onBackButtonPress={onBackButtonPress}
    />
) : null}

Update the below in src/pages/iou/request/step/StepScreenWrapper.tsx:

Line no 36: 
/** Hide the header when the wrapper is shown */
hideHeader?: boolean;
Line no 50: hideHeader,
Line no 68:
{!hideHeader ? (
    <HeaderWithBackButton
        title={headerTitle}
        onBackButtonPress={onBackButtonPress}
    />
) : null}

The id & other props are passed directly in the above changes for the purpose of creating a PoC. The additional ScreenWrapper in the IOURequestStartPage page is also removed with the above approach.

What alternative solutions did you explore? (Optional)

We can fix this issue also by not rendering the contents of other screens i.e. when the Manual screen is in view, the contents of the Split & Distance screens shouldn't be rendered.

  • Since the TAB navigation focus is already handled by focus-trap library, we can remove the below code block.
useFocusEffect(
    useCallback(() => {
        const handler = (event: KeyboardEvent) => {
            if (event.code !== CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) {
                return;
            }
            event.preventDefault();
            event.stopPropagation();
        };
        KeyDownPressListener.addKeyDownPressListener(handler);

        return () => KeyDownPressListener.removeKeyDownPressListener(handler);
    }, []),
);

The fix for the issue is to only include the interactive elements of the focussed tab in the TAB navigation behavior order i.e. when the Manual tab is selected, the interactive elements of the Split & Distance tab should be removed from the TAB navigation tree by visually hiding them.

<OnyxTabNavigator
    id={CONST.TAB.IOU_REQUEST_TYPE}
    onTabSelected={resetIOUTypeIfChanged}
    tabBar={TabSelector}
>
    <TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>
        {(props) => (
            <View style={{visibility: props?.navigation?.isFocused() ? 'visible' : 'hidden', flex: 1}}>
                <IOURequestStepAmount
                    shouldKeepUserInput
                    route={route}
                />
            </View>
        )}
    </TopTab.Screen>
    <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>
        {(props) => (
            <View style={{visibility: props?.navigation?.isFocused() ? 'visible' : 'hidden', flex: 1}}>
                <IOURequestStepScan route={route} />
            </View>
        )}
    </TopTab.Screen>
    {shouldDisplayDistanceRequest && (
        <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>
            {(props) => (
                <View style={{visibility: props.navigation.isFocused() ? 'visible' : 'hidden', flex: 1}}>
                    <IOURequestStepDistance route={route} />
                </View>
            )}
        </TopTab.Screen>
    )}
</OnyxTabNavigator>

With the above update, the contents of all the tabs are never re-rendered and only the visible elements are included in the TAB navigation tree.

Copy link

melvin-bot bot commented Jun 14, 2024

📣 @vishnu-karuppusamy! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:

  1. Make sure you've read and understood the contributing guidelines.
  2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
  3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
  4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
    Screen Shot 2022-11-16 at 4 42 54 PM
    Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>

@vishnu-karuppusamy
Copy link

Contributor details
Your Expensify account email: vishnu566662@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01fded0233eaf38db6

Copy link

melvin-bot bot commented Jun 14, 2024

✅ Contributor details stored successfully. Thank you for contributing to Expensify!

Copy link

melvin-bot bot commented Jun 27, 2024

@tgolen @OfstadC @rayane-djouah this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!

@rayane-djouah
Copy link
Contributor

Posted in Slack to get more 👀: https://expensify.enterprise.slack.com/archives/C01GTK53T8Q/p1719429382691979

@OfstadC
Copy link
Contributor

OfstadC commented Jul 1, 2024

Any update @rayane-djouah ? 😃

@melvin-bot melvin-bot bot added the Overdue label Jul 1, 2024
@rayane-djouah
Copy link
Contributor

Waiting on proposals

@melvin-bot melvin-bot bot removed the Overdue label Jul 1, 2024
@rayane-djouah
Copy link
Contributor

@OfstadC - can we consider raising the bounty to get more proposals?

Copy link

melvin-bot bot commented Jul 4, 2024

📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

@OfstadC
Copy link
Contributor

OfstadC commented Jul 9, 2024

@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.

@melvin-bot melvin-bot bot added the Overdue label Jul 9, 2024
@OfstadC
Copy link
Contributor

OfstadC commented Jul 9, 2024

Cool it Melvin 😅

Copy link

melvin-bot bot commented Jul 9, 2024

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

@trjExpensify
Copy link
Contributor

^^ Dammit, the sidepane jumped as I was trying to click the project. 😂

@rayane-djouah
Copy link
Contributor

Thank you for the update

@melvin-bot melvin-bot bot removed the Overdue label Jul 9, 2024
Copy link

melvin-bot bot commented Jul 11, 2024

📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

@OfstadC
Copy link
Contributor

OfstadC commented Jul 11, 2024

I imagine a "Polish" doesn't need to be increased, but curious your thoughts @trjExpensify 😄

Copy link

melvin-bot bot commented Jul 11, 2024

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

  • Finding a contributor to fix the bug
  • Closing the issue if BZ has been unable to add the issue to a VIP or Wave project
  • If you have any questions, don't hesitate to start a discussion in #expensify-open-source

Thanks!

@melvin-bot melvin-bot bot added the Overdue label Jul 11, 2024
@rayane-djouah
Copy link
Contributor

Started a discussion in #expensify-open-source

@melvin-bot melvin-bot bot removed the Overdue label Jul 11, 2024
@trjExpensify
Copy link
Contributor

I imagine a "Polish" doesn't need to be increased, but curious your thoughts @trjExpensify 😄

Agree.

@dominictb
Copy link
Contributor

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:

  • All tabbable elements from 3 tabs are included in the focus trap, along side with the header back button. And all of them are in 1 single container group.

  • Looking into the focus-trap implementation here and here: if the element is in the rear end of a group, focus-trap lib will move the focus programmatically (see this and this, it basically move the focus to the first/last element of the next/prev group depending on whether we are TAB-ing forward or SHIFT+TAB-ing backward), but if there is no group jumping (we are from one focusable element to the next in the same container group), the focus-trap will depend on the browser native TAB/SHIFT+TAB behavior to move the focus for us, unless we explicitly
    configured a different key event for moving focus in focus trap (check this)

  • Hence, if we try tab, the focus will be on the header back button (the first focusable element in the focus trap), and tab-ing further won't move the focus anywhere else, because we have already disabled the default TAB/SHIFT TAB behavior here.

  • If we try SHIFT+TAB, the focus will be on the last element in the focus trap, i.e: the Next button of the third tab Distance. And as the focus-trap do this programmatically, it will call the node.focus({preventScroll: false}) here, this will force the Next button in the Distance tab, which is currently out of view, scroll into visible port and shifting the UI to the left. Hence, the tabs seem to disappear in this case.

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

We have 3 options:

  • We should add a props in ScreenWrapper to allow disabling the focus trap. I'm not sure if it is intended to have the focus trap in IOURequestStartPage, but if we don't need it then we should disable it to fix this issue.

  • We can add a props focusTrapOptionsPassthrough in ScreenWrapper, and in IOURequestStartPage, set

<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.

  • If the focus-trap is needed in IOURequestStartPage, but we don't want to include focusable element from inactive tabs, we can consider render only the active tab. It can be done by adding a prop to 3 component IOURequestStepAmount, IOURequestStepScan, IOURequestStepDistance :
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)

@melvin-bot melvin-bot bot added the Overdue label Jul 15, 2024
Copy link

melvin-bot bot commented Jul 15, 2024

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

@rayane-djouah
Copy link
Contributor

rayane-djouah commented Jul 16, 2024

@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?

@melvin-bot melvin-bot bot removed the Overdue label Jul 16, 2024
@dominictb
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2 Engineering External Added to denote the issue can be worked on by a contributor Help Wanted Apply this label when an issue is open to proposals by contributors
Projects
Status: Polish
Development

No branches or pull requests

9 participants