Tried to implement notification page with tabs inside it, the tabs are filters based on categories. I'd like to have swipe capability using Swiper JS as suggested in doc https://ionicframework.com/docs/api/slides and it worked well.
Then I have to put ionic refresher also inside all slides so that I can refresh the data refetching as I pull down. However, the pulling down gesture was not working at all.
Is it something to do with the styles or the stack of the element, not sure.
The expected is I'll have two gesture, horizontal to switch over tabs, vertical to pull down refresh.
Notification page
import React, { useEffect, useMemo, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import { TopHeader } from "../../ui/layouts/Headers";
import NotificationList from "../../ui/notifications/NotificationList";
import Tabs from "../../ui/core/Tabs";
import { useHistory, useLocation } from "react-router-dom";
import qs from "query-string";
const NotificationPage = () => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const categories = useMemo<string[]>(
() => ["All", "Promo", "Info", "Reminder"],
[]
);
const history = useHistory();
const location = useLocation();
const queryParams = qs.parse(location.search);
useEffect(() => {
if (Object.keys(queryParams)[0] === "category") {
setSelectedTab(
categories.findIndex((cat) => cat === (queryParams.category as string))
);
} else {
history.replace({
pathname: "/notifications",
search: "?category=All",
});
}
}, [queryParams, history, categories]);
return (
<IonPage>
<TopHeader pageName={"Notifications"} />
<IonContent>
<div className="pt-2 h-[calc(100%-74px)]">
<Swiper
className="h-full"
slidesPerView={1}
onActiveIndexChange={(e) => {
history.replace({
pathname: "/notifications",
search: `?category=${categories[e.activeIndex]}`,
});
setSelectedTab(e.activeIndex);
}}
initialSlide={categories.findIndex(
(cat) => cat === queryParams.category
)}
>
<div
slot="container-start"
className="flex flex-row gap-2 mb-2 justify-center items-center"
>
<Tabs tabs={categories} gap swiper selected={selectedTab} />
</div>
{categories.map((cat, index) => (
<SwiperSlide
key={index}
className="!h-[calc(100%-46px)] overflow-y-auto scrollbar-default"
>
<NotificationList category={cat} />
</SwiperSlide>
))}
</Swiper>
</div>
</IonContent>
</IonPage>
);
};
export default NotificationPage;
...
Notification List
import React, { useCallback, useEffect, useState } from "react";
...
interface NotificationListProps {
category: string;
}
const NotificationList = ({ category }: NotificationListProps) => {
const [entityId, setEntityId] = useState<string>("");
const [notifications, setNotifications] = useState<NotificationMessage[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const location = useLocation();
const queryParams = qs.parse(location.search);
...
const listNotifications = useCallback(async () => {
try {
if (entityId) {
if (category === queryParams.category) {
const listNotificationsRes = await Urql.query(
listNotificationMessagesQuery(entityId, category.toLowerCase())
);
const listNotificationsData =
listNotificationsRes.data.listMessages.items;
setNotifications(listNotificationsData);
return setLoading(false);
}
}
setLoading(false);
} catch (error) {
toast.error(error.message, toastProperties);
setLoading(false);
}
}, [category, queryParams.category, entityId]);
...
useEffect(() => {
if (loading) {
listNotifications();
}
}, [listNotifications, loading]);
return (
<ContentRefreshable setRefresh={setLoading}>
<div className="px-4 w-full mb-2">
{loading || !notifications.length ? (
<NotificationlistSkeleton />
) : (
<div className="flex flex-col gap-2 justify-center">
{notifications.map((notif: NotificationMessage) => (
<NotificationCard
key={notif.id}
entityId={entityId}
notification={notif}
/>
))}
</div>
)}
</div>
</ContentRefreshable>
);
};
export default NotificationList;
...
Ion Refresher component
import { IonContent, IonRefresher, IonRefresherContent } from "@ionic/react";
import { RefresherEventDetail } from "@ionic/core";
import { Dispatch, ReactNode, SetStateAction, useState } from "react";
import PageLoader from "../loader/Loader";
interface IonRefreshProps {
children: ReactNode;
setRefresh?: Dispatch<SetStateAction<boolean>>;
}
const ContentRefreshable = ({ children, setRefresh }: IonRefreshProps) => {
const [loading, setLoading] = useState(false);
const doRefresh = (event: CustomEvent<RefresherEventDetail>) => {
setLoading(true);
event.detail.complete();
setLoading(false);
};
const doRefreshProps = (event: CustomEvent<RefresherEventDetail>) => {
setRefresh(true);
event.detail.complete();
};
return (
<IonContent>
<IonRefresher
slot="fixed"
onIonRefresh={setRefresh ? doRefreshProps : doRefresh}
pullFactor={0.5}
pullMin={100}
pullMax={200}
>
<IonRefresherContent></IonRefresherContent>
</IonRefresher>
{loading ? <PageLoader /> : children}
</IonContent>
);
};
export default ContentRefreshable;