Browse Source

fix(navigation): remove history stack pollution in useSheetScrollLock and overlays to fix hardware back sheet closing

master
mortezaei 2 days ago
parent
commit
a8423a2d89
  1. 10
      src/app/intro/intro-client.tsx
  2. 10
      src/components/Componentes/marriage-advisors-overlay.tsx
  3. 10
      src/components/Componentes/match-profile-overlay.tsx
  4. 2
      src/components/Componentes/question-birthplace.tsx
  5. 2
      src/components/Componentes/question-date-sheet.tsx
  6. 2
      src/components/Componentes/question-phone.tsx
  7. 13
      src/components/Componentes/question-sheet.test.tsx
  8. 29
      src/components/Componentes/use-sheet-scroll-lock.ts

10
src/app/intro/intro-client.tsx

@ -58,20 +58,20 @@ export default function IntroClient() {
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.set("steps", "open");
window.history.pushState({ steps: "open" }, "", url.toString());
window.history.replaceState({ steps: "open" }, "", url.toString());
}
}, []);
const handleCloseSteps = useCallback(() => {
setIsStepsOpen(false);
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
if (params.get("steps") === "open") {
setIsStepsOpen(false);
window.history.back();
return;
const url = new URL(window.location.href);
url.searchParams.delete("steps");
window.history.replaceState({}, "", url.toString());
}
}
setIsStepsOpen(false);
}, []);
useHardwareBackHandler(() => {

10
src/components/Componentes/marriage-advisors-overlay.tsx

@ -34,20 +34,20 @@ export function useMarriageAdvisorsOverlay() {
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.set("advisors", "open");
window.history.pushState({ advisors: "open" }, "", url.toString());
window.history.replaceState({ advisors: "open" }, "", url.toString());
}
}, []);
const closeAdvisors = useCallback(() => {
setIsAdvisorOpen(false);
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
if (params.get("advisors") === "open") {
setIsAdvisorOpen(false);
window.history.back();
return;
const url = new URL(window.location.href);
url.searchParams.delete("advisors");
window.history.replaceState({}, "", url.toString());
}
}
setIsAdvisorOpen(false);
}, []);
// Intercept hardware back in Flutter WebView when advisor overlay is open

10
src/components/Componentes/match-profile-overlay.tsx

@ -34,20 +34,20 @@ export function useMatchProfileOverlay() {
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.set("profile", "open");
window.history.pushState({ profile: "open" }, "", url.toString());
window.history.replaceState({ profile: "open" }, "", url.toString());
}
}, []);
const closeProfile = useCallback(() => {
setIsProfileOpen(false);
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
if (params.get("profile") === "open") {
setIsProfileOpen(false);
window.history.back();
return;
const url = new URL(window.location.href);
url.searchParams.delete("profile");
window.history.replaceState({}, "", url.toString());
}
}
setIsProfileOpen(false);
}, []);
// Intercept hardware back in Flutter WebView when profile overlay is open

2
src/components/Componentes/question-birthplace.tsx

@ -249,7 +249,7 @@ export function QuestionBirthplace({
setIsClosing(false);
}, [disabled]);
useSheetScrollLock(isOpen, { onBack: closeSheet });
useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet });
// Handle escape key
useEffect(() => {

2
src/components/Componentes/question-date-sheet.tsx

@ -181,7 +181,7 @@ export function QuestionDateSheet({
window.setTimeout(onClose, EXIT_ANIMATION_MS);
}, [onClose]);
useSheetScrollLock(true, { onBack: closeSheet });
useSheetScrollLock(!isClosing, { onBack: closeSheet });
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {

2
src/components/Componentes/question-phone.tsx

@ -586,7 +586,7 @@ export function QuestionPhone({
);
}, [countryList, searchQuery]);
useSheetScrollLock(isOpen, { onBack: closeSheet });
useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet });
useEffect(() => {
if (!isOpen) return;

13
src/components/Componentes/question-sheet.test.tsx

@ -192,7 +192,7 @@ describe("QuestionSheet component", () => {
});
});
it("closes the sheet instead of leaving the page on browser Back", async () => {
it("closes the sheet instead of leaving the page on hardware Back", async () => {
const question = {
id: "q_back",
title: "کشور",
@ -202,7 +202,6 @@ describe("QuestionSheet component", () => {
options: [{ id: "iran", value: "Iran", label: "ایران", order: 1 }],
ui_config: {},
} as QuestionField;
const pushState = vi.spyOn(window.history, "pushState");
render(
<QueryClientProvider client={queryClient}>
@ -213,15 +212,17 @@ describe("QuestionSheet component", () => {
);
fireEvent.click(screen.getByRole("button", { name: /انتخاب کشور/i }));
await waitFor(() => expect(pushState).toHaveBeenCalledTimes(1));
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
window.history.replaceState(null, "", window.location.href);
window.dispatchEvent(new PopStateEvent("popstate"));
const { handleHardwareBackSync } = await import("@/hooks/use-hardware-back-handler");
const handled = handleHardwareBackSync();
expect(handled).toBe(true);
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
});
pushState.mockRestore();
});
it("opens a searchable sheet for 7+ options without focusing search", () => {

29
src/components/Componentes/use-sheet-scroll-lock.ts

@ -10,7 +10,6 @@ let initialHtmlOverflow = "";
let initialAppShellOverflow = "";
let initialAppShellTouchAction = "";
let lockedAppShell: HTMLElement | null = null;
const SHEET_HISTORY_KEY = "__habibQuestionSheet";
type SheetScrollLockOptions = {
onBack?: () => void;
@ -25,7 +24,7 @@ export function useSheetScrollLock(
onBackRef.current = onBack;
// Register in the hardware-back handler stack so that Flutter's
// __habibHandleHardwareBack() closes the sheet instead of navigating.
// __habibHandleHardwareBackSync() closes the sheet instead of exiting the screen.
const handleHardwareBack = useCallback(() => {
if (onBackRef.current) {
onBackRef.current();
@ -39,18 +38,6 @@ export function useSheetScrollLock(
useEffect(() => {
if (!isOpen) return;
const historyState = window.history.state;
const ownsHistoryEntry =
historyState?.[SHEET_HISTORY_KEY] !== true && activeSheetCount === 0;
if (ownsHistoryEntry) {
window.history.pushState(
{ ...(historyState ?? {}), [SHEET_HISTORY_KEY]: true },
"",
window.location.href,
);
}
if (activeSheetCount === 0) {
bodyHadDropdownClass = document.body.classList.contains("dropdown-open");
initialBodyOverflow = document.body.style.overflow;
@ -70,23 +57,9 @@ export function useSheetScrollLock(
lockedAppShell.style.touchAction = "none";
}
const handlePopState = () => {
if (ownsHistoryEntry) {
onBackRef.current?.();
}
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
activeSheetCount = Math.max(0, activeSheetCount - 1);
if (activeSheetCount === 0) {
if (
ownsHistoryEntry &&
window.history.state?.[SHEET_HISTORY_KEY] === true
) {
window.history.back();
}
document.body.style.overflow = initialBodyOverflow;
document.documentElement.style.overflow = initialHtmlOverflow;
if (lockedAppShell) {

Loading…
Cancel
Save