Next.js: Get back to old scroll position on back button WITHOUT affecting the other/next page scroll? - scroll

Is there a possibility in next.js to get back to old scroll position on back button WITHOUT affecting the other/next page scroll?
The solution I tried is working but not without affecting the next page scroll position and also the next page affecting the previous/back button page scroll postion.
So if i do not scroll on next page and go back it's fine, but I scroll on next page and get back the position of back page(original page) is not the same
What I tried:
Update next.config.js with:
experimental: {
modern: true,
scrollRestoration: true,
}
Switched scroll prop to false for SiteLink component which wraps Link from next:
<SiteLink href={slug} scroll={false} />
Attach and remove scroll event listener on page I want scroll to be remebered:
const state = useContext(ScrollContext);
useEffect(() => {
window.scrollTo(0, state.scrollPos);
const handleScrollPos = () => {
state.scrollPos = window.scrollY;
};
window.addEventListener('scroll', () => handleScrollPos());
return () => {
// remove event listener on unmount
window.removeEventListener('scroll', () => handleScrollPos());
};
}, []);
Scroll Context
import { createContext } from 'react';
const state = {
scrollPos: 0,
};
const ScrollContext = createContext(state);
export default ScrollContext;```

Related

How can I navigate to a nested stack screen inside a sibling tab while preserving its initial screen? (React Navigation)

The Code:
I'm using React Navigation 6 with the following hierarchy:
MainTabNavigator
HomeStack
HomeScreen (HomeStack initial screen, contains a "Pay" button)
OtherScreen
MembershipStack
MembershipHomeScreen (MembershipStack initial screen)
PayMembershipScreen (should always navigate back to MembershipHomeScreen)
The App launches into HomeScreen (inside HomeStack), so the MembershipStack tab won't be loaded yet.
There is a "Pay" button inside HomeScreen that redirects to PayMembershipScreen inside MembershipStack.
Code of the HomeScreen with the "Pay" button press handler:
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
// The parent navigator here is MainTabNavigator
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
The "Pay" button inside HomeScreen does navigate to PayMembershipScreen with this code.
The Problem:
When the MembershipStack tab has not yet been loaded (i.e. when the App just launched), the following happens:
User clicks the "Pay" button on HomeScreen.
App navigates to PayMembershipScreen.
PayMembershipScreen becomes the initial screen on MembershipStack.
User can't go back to MembershipHomeScreen from PayMembershipScreen.
The question is: how can I navigate from HomeScreen directly to PayMembershipScreen and after that be able to go back to MembershipHomeScreen (i.e. have it available as the initial screen in the MembershipStack history)?
What I've tried so far:
1. Setting lazy: false on the MainTabNavigator options.
This approach does make sure that MembershipHomeScreen is always the initial screen on MembershipStack, since all stacks (and their initial screens) will be loaded when the App launches. However this comes with a noticeable drawback in performance, since there's at least 5 tabs in the actual project.
Also, since MembershipHomeScreen is already focused inside MembershipStack, there's a weird MembershipHomeScreen to PayMembershipScreen transition animation when the "Pay" button on HomeScreen is pressed. I just want the user to see a transition from HomeScreen to PayMembershipScreen at most, nothing flashing inbetween.
2. Define a param on MembershipHomeScreen to indicate when I want to redirect to PayMembershipScreen.
On this approach, I'm using a boolean param on MembershipHomeScreen called redirectToPayment:
Code inside MembershipHomeScreen:
const MembershipHomeScreen = ({navigation, route}) => {
const { redirectToPayment = false } = route.params
const redirectIfNeeded = useCallback(() => {
if (redirectToPayment) {
// reset the param
navigation.setParams({ redirectToPayment: false })
// redirect to the desired screen
navigation.navigate("PayMembershipScreen")
}
}, [redirectToPayment, navigation])
// Using layout effect to avoid rendering MembershipHomeScreen when redirecting.
useLayoutEffect(redirectIfNeeded)
return (
<Text>
This is just the membership home screen,
not the target screen of the "Pay" button.
</Text>
)
}
And on the HomeScreen:
const payPressHandler = useCallback(() => {
navigation
.getParent()
.navigate(
"MembershipStack",
{ screen: "MembershipHomeScreen", params: { redirectToPayment: true } }
)
}, [navigation])
The use of React's useLayoutEffect comes with the known drawback of freezing the screen since it will leave any rendering tasks on hold while it's running. I'm able to notice a 2 seconds freeze in the HomeScreen when I press the "Pay" button on a 4GB RAM Moto G7...
...and even after using useLayoutEffect, the MembershipHomeScreen still renders nonetheless to show a transition animation between MembershipHomeScreen and PayMembershipScreen.
Same behavior with useEffect, except it renders the MembershipHomeScreen instead of a 2 seconds freeze.
3. Using React Navigation's dispatch function to customize the route history.
On this approach, I intend to dispatch a custom action that does the following to the navigation state of MainTabNavigator:
Before:
index: 0
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home" // <-- currently active screen
}
]
}
},
1: {
name: "MembershipStack",
state: undefined // <-- this is an unloaded tab
}
]
After:
index: 1
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home"
}
]
}
},
1: {
name: "MembershipStack",
state: {
index: 1,
routes: [
0: {
name: "MembershipHomeScreen"
},
1: {
name: "PayMembershipScreen" // <-- currently active screen
}
]
}
},
]
Here's the code I'm using inside the HomeScreen for that:
const payPressHandler = useCallback(() => {
navigation.getParent().dispatch(state => {
const membershipTabIndex = state.routes.findIndex(r => r.name === "MembershipStack")
// get the current navigation state inside the MembershipStack
let membershipTabState = state.routes[membershipTabIndex].state
// point to PayMembershipScreen without overriding the initial screen on that tab
if (!membershipTabState) {
// tab is unloaded, so just set the ideal state
membershipTabState = { index: 1, routes: [{ name: "MembershipHomeScreen" }, { name: "PayMembershipScreen" }] }
} else {
// tab already has a navigation state, so we'll point to PayMembershipScreen
// if it's loaded in the stack. Otherwise, we'll add it and point to it.
let payMembershipScreenIndex = membershipTabState.routes.findIndex(r => r.name === "PayMembershipScreen")
if (payMembershipScreenIndex === -1) {
payMembershipScreenIndex = membershipTabState.routes.push({ name: "PayMembershipScreen" }) - 1
}
membershipTabState.index = payMembershipScreenIndex
}
// update the MembershipStack tab with the new state
const routes = state.routes.map((r, i) => i === membershipTabIndex ? { ...r, state: membershipTabState} : r)
// update the MainTabNavigator state
return CommonActions.reset({
...state,
routes,
index: membershipTabIndex
})
})
}, [navigation])
That code almost accomplishes the expected outcome:
It navigates from HomeScreen to PayMembershipScreen successfuly.
The user can go back to MembershipHomeScreen from PayMembershipScreen.
MembershipHomeScreen does not render in-between during the transition.
However:
It won't work a second time if you do go back to MembershipHomeScreen from PayMembershipScreen.
Turns out, if I go back to MembershipHomeScreen once I'm inside PayMembershipScreen, then go back to the HomeStack and press the "Pay" button again, it will now navigate to MembershipHomeScreen instead of PayMembershipScreen.
Additionally, the MembershipHomeScreen will now display a disabled back button in the header (probably a bug).
This last approach is so far the closest to getting the desired outcome, so I really hope that it only needs a fix in the logic and it's not really a bug.
Summary:
Is anyone able find a solution that achieves the expected outcome? To sum up:
It should make the "Pay" button navigate from HomeScreen to PayMembershipScreen.
The user should be able to go back to MembershipHomeScreen once they're in PayMembershipScreen.
The MembershipHomeScreen should not flash in the transition from HomeScreen to PayMembershipScreen.
The screen should not freeze inbetween the transitions (no use of useLayoutEffect).
Don't load all tabs on App launch (it's too much burden on the actual 5-tab project).
If the user navigates back to MembershipHomeScreen once they're inPayMembershipScreen, pressing the "Pay" button on HomeScreen again should open the PayMembershipScreen again (no buggy behavior).
Minimal reproducible example:
Here's a snack with all of the approaches mentioned. Please check it out and use it as a playground for your solution!
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button
Solution
I got the expected outcome with the following approach:
Navigate directly to PayMembershipScreen when the "Pay" button is pressed.
Inside PayMembershipScreen, add MembershipHomeScreen to the top of MembershipStack history if not already present.
The Code
Code inside HomeScreen (no changes):
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
New useAssertInitialScreen hook:
const useAssertInitialScreen = ({ navigation, name }) => {
useEffect(() => {
if (navigation.canGoBack() === false) {
navigation.dispatch((state) => {
const routes = [{ name }, ...state.routes]
return CommonActions.reset({
...state,
routes,
index: state.index + 1
})
})
}
}, [navigation])
}
Note: I'm passing the navigation prop down the hook since it's also used in the screen, but it could also be obtained from React Navigation's useNavigation hook.
Code inside PayMembershipScreen:
const PayMembershipScreen = ({navigation}) => {
useAssertInitialScreen({ navigation, name: "MembershipHomeScreen" })
return (
<View>
<Text>
This is the PayMembershipScreen AKA the target
</Text>
</View>
)
}
Demo
Here's a snack with the code of this answer:
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button---a-solution
I still want to see whether there's a cleaner solution than handling the "Pay" button's functionality inside its target screen.
I'll mark as the accepted answer any other solution that gets the expected outcome :)

How to write an If/ Else statement in Cypress for finding a button?

I have a page that can hold 25 items and when there are more than 25, then it goes onto the next page, how can I write an if statement to first check if the ‘Next Page Button’ is enabled/ visible and if it is visible/enabled then to click on it and if it is not visible, then to find the item on the current page it is on?
If the Next Page button is enabled/disabled then you can do:
cy.get('next-page-button').then(($btn) => {
if ($btn.is(':enabled')) {
cy.wrap($btn).click() //Click on the next page button
} else {
//Do something when the button is disabled
}
})
If the Next Page button exists/Doesn't exists:
cy.get('body').then(($body) => {
if ($body.find('next-page-button').length > 0) {
cy.get('next-page-button').click() //Button exists and click on the button
} else {
//Do something when the button doesn't exist
}
})
You will need to wait for the page to refresh each time the button is clicked.
If you have an element that indicates the page number, use that. If not you will need to add a hard wait.
const clickUntilDisabled = ($button, page = 1) => {
if (page > 100) return // make sure it does not keep clicking forever
if ($button.is(':enabled') && $button.is(':visible')) {
$button.click()
// page refeshes
cy.contains(`Page ${++page}`) // check the page indicator
// or cy.wait(500)
clickUntilDisabled($button, page) // click again
}
}
cy.contains('button', 'Next Page Button')
.then(clickUntilDisabled)
.then(() => {
// test item here
})

Unable to Drag and drop into iframe using cypress

Hi I have to drag a component which is outside a frame and drop into a iframe body(dragable source)
but it is not draging my component into iframe dragable source
this is code which is am using for get into the iframe and it is working means it is going into the iframe and finding element
const getIframeDocument = () => {
return cy.get(".gjs-frame").its("0.contentDocument").should("exist");
};
const getIframeBody = () => {
// get the document
return (
getIframeDocument()
// automatically retries until body is loaded
.its("body")
.should("not.be.undefined")
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
);
};
and
for drag and drop into iframe like this
getIframeBody().find("#wrapper").as("Target");
cy.get('[title="Url"]').drag("#Target");
this is not giving me any error this drag function working but at the same time drag and drop is not happening
drag and drop execution pic must see this
you can do this by using this code. You just need to change selectors according to your website.
// Define Locators.
drag_element = '.blockbuilder-content-tools .blockbuilder-content-tool:nth-
child(6)';
drop_element = '#u_body #u_row_2';
text_indentation = '[aria-label="Align center"]';
new_box_text = 'This is a new Text block. Change the text.'
// Get iframe body and then drag & drop element.
iframe_interaction(){
let ifm = cy.get('iframe');
ifm
.should(iframe => expect(iframe.contents().find('body')))
.then(iframe => cy.wrap(iframe.contents().find('body')))
.within({}, $iframe => {
cy.get(this.drag_element).drag(this.drop_element, { force: true })
cy.contains(this.new_box_text).dblclick({force: true})
cy.get(this.text_indentation).click()
})
Github Link: https://github.com/sardar-usman/Cypress--Drag-and-Drop

Altering drag drop image in Angular environment

I'm working in Angular 8, jqgrid working perfectly.
Is there a way I can alter the drag and drop image when DnD between two grids?
I'm playing with:
this.jqgSource.jqGrid(
'gridDnD',
{
connectWith: this.jqgSelectorTarget,
onstart: (ev, ui) => {
$(this).html("new content here");
},
ondrop: (ev, ui, dragdata) => {
const registrationId = dragdata.registrationId;
const moveToTeamId = this.targetTeamId;
this.swapComplete(registrationId, moveToTeamId);
}
});
but no luck...
Oleg got me going.
To change the content of the dragged object
Instead of:
$(this).html("new content here")
I needed to access the ui.helper:
ui.helper.html("new content here")

Nativescript angular : handle android back button on different pages

So I use this function to handle android back button :
this._page.on(Page.loadedEvent, event => {
if (application.android) {
application.android.on(application.AndroidApplication.activityBackPressedEvent, (args:AndroidActivityBackPressedEventData) => {
args.cancel = true;
this._ngZone.run(() => {
this.router.navigate(['/parameters']);
});
});
}
})
on different pages (angular components). So on page1.ts I have navigate(['/parameters]) and on page2.ts I have console.log("test"). Problem is wherever I am in the app, pressing back button always do navigate(['/parameters]), also the console.log if i'm on the right page, but it should do console.log only.
It seems to be global, any idea how to override activityBackPressedEvent ?
activityBackPressedEvent is not specific to a page, it's global to your Activity which holds all the pages. Generally, You will not add more than one event listener to this event.
You could do something like below to handle this on page level, probably in app module / main.ts
application.android.on(application.AndroidApplication.activityBackPressedEvent,
(args: application.AndroidActivityBackPressedEventData) => {
const page = frame.topmost().currentPage;
if (page.hasListeners(application.AndroidApplication.activityBackPressedEvent)) {
args.cancel = true;
page.notify({
eventName: application.AndroidApplication.activityBackPressedEvent,
object: page
});
}
});
With above code, activityBackPressedEvent willl be triggered on every page that has a listener.
Now in your page / component in which you want to customise the behaviour you do this,
// Inject Page
constructor(private page: Page) {
this.page.on(application.AndroidApplication.activityBackPressedEvent, this.onBackButtonTap, this);
}
onBackButtonTap(data: EventData) {
this._ngZone.run(() => {
this.router.navigate(['/parameters']);
});
}
I think since you added the handle back button in the event pageLoaded that's why it does not work on other page.
The code that handle back button should be placed in the app starter. I'm using NS Vue & I place this code in my main.js. I think it could be similar in NS angular.
application.android.on(application.AndroidApplication.activityBackPressedEvent, (args:AndroidActivityBackPressedEventData) => {
args.cancel = true;
this._ngZone.run(() => {
this.router.navigate(['/parameters']);
});
});

Resources