{
afterEach(() => {
cleanup();
document.body.classList.remove("dropdown-open");
- document.body.classList.remove("question-input-open");
+ document.body.classList.remove("question-sheet-open");
+ document.body.classList.remove("question-keyboard-open");
});
const queryClient = new QueryClient({
@@ -77,7 +78,7 @@ describe("QuestionSheet component", () => {
// Bottom sheet dialog should be visible
expect(screen.getByRole("dialog")).toBeDefined();
expect(document.body.classList.contains("dropdown-open")).toBe(true);
- expect(document.body.classList.contains("question-input-open")).toBe(true);
+ expect(document.body.classList.contains("question-sheet-open")).toBe(true);
expect(
(document.querySelector(".app-shell") as HTMLElement).style.overflowY,
).toBe("hidden");
@@ -92,7 +93,7 @@ describe("QuestionSheet component", () => {
await waitFor(() => {
expect(screen.getByText("ایران")).toBeDefined();
expect(document.body.classList.contains("dropdown-open")).toBe(false);
- expect(document.body.classList.contains("question-input-open")).toBe(
+ expect(document.body.classList.contains("question-sheet-open")).toBe(
false,
);
expect(
@@ -174,9 +175,73 @@ describe("QuestionSheet component", () => {
await waitFor(() => {
expect(screen.queryByRole("dialog")).toBeNull();
expect(document.body.classList.contains("dropdown-open")).toBe(false);
- expect(document.body.classList.contains("question-input-open")).toBe(
+ expect(document.body.classList.contains("question-sheet-open")).toBe(
false,
);
});
});
+
+ it("opens a searchable sheet without focusing search", () => {
+ const question = {
+ id: "q_country",
+ title: "کشور",
+ type: "dropdown",
+ required: true,
+ extras: { placeHolder: "انتخاب کشور" },
+ options: Array.from({ length: 6 }, (_, index) => ({
+ id: `country-${index}`,
+ value: `country-${index}`,
+ label: `کشور ${index + 1}`,
+ order: index + 1,
+ })),
+ ui_config: {},
+ } as QuestionField;
+
+ render(
+
+
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /انتخاب کشور/i }));
+
+ expect(screen.getByPlaceholderText("جستجو...")).not.toHaveFocus();
+ expect(screen.getByRole("dialog").querySelector("section")).toHaveClass(
+ "h-[82svh]",
+ );
+ });
+
+ it("sizes a short options sheet to its content", () => {
+ const question = {
+ id: "q_short",
+ title: "انتخاب کوتاه",
+ type: "dropdown",
+ required: true,
+ extras: { placeHolder: "انتخاب" },
+ options: Array.from({ length: 4 }, (_, index) => ({
+ id: `option-${index}`,
+ value: `option-${index}`,
+ label: `گزینه ${index + 1}`,
+ order: index + 1,
+ })),
+ ui_config: {},
+ } as QuestionField;
+
+ render(
+
+
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
+
+ expect(screen.getByRole("dialog").querySelector("section")).toHaveClass(
+ "h-auto",
+ "max-h-[82svh]",
+ );
+ });
});
diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx
index bf8fc8d..86efa9d 100644
--- a/src/components/Componentes/question-sheet.tsx
+++ b/src/components/Componentes/question-sheet.tsx
@@ -37,7 +37,6 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
- const searchInputRef = useRef(null);
const listRef = useRef(null);
const closeSheet = useCallback(() => {
setIsClosing(true);
@@ -76,6 +75,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
if (question.extras?.noSearch) return false;
return options.length > 5;
})();
+ const isCompact = options.length <= 5;
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchQuery.toLowerCase()),
@@ -201,7 +201,8 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
>
@@ -260,7 +261,6 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
/>
setSearchQuery(e.target.value)}
diff --git a/src/components/Componentes/use-sheet-scroll-lock.ts b/src/components/Componentes/use-sheet-scroll-lock.ts
index eb2b60f..2ff82a9 100644
--- a/src/components/Componentes/use-sheet-scroll-lock.ts
+++ b/src/components/Componentes/use-sheet-scroll-lock.ts
@@ -1,9 +1,10 @@
"use client";
import { useEffect } from "react";
+import { viewPaddingsBridge } from "@/lib/view-paddings";
let activeSheetCount = 0;
-let activeKeyboardInputCount = 0;
+let hasFocusedKeyboardInput = false;
let bodyHadDropdownClass = false;
let initialBodyOverflow = "";
let initialHtmlOverflow = "";
@@ -26,7 +27,7 @@ export function useSheetScrollLock(isOpen: boolean) {
activeSheetCount += 1;
document.body.classList.add("dropdown-open");
- document.body.classList.add("question-input-open");
+ document.body.classList.add("question-sheet-open");
document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden";
if (lockedAppShell) {
@@ -47,9 +48,7 @@ export function useSheetScrollLock(isOpen: boolean) {
if (!bodyHadDropdownClass) {
document.body.classList.remove("dropdown-open");
}
- if (activeKeyboardInputCount === 0) {
- document.body.classList.remove("question-input-open");
- }
+ document.body.classList.remove("question-sheet-open");
}
};
}, [isOpen]);
@@ -87,10 +86,10 @@ function isKeyboardInputTarget(target: EventTarget | null): boolean {
}
function syncQuestionInputOpenClass() {
- if (activeSheetCount > 0 || activeKeyboardInputCount > 0) {
- document.body.classList.add("question-input-open");
+ if (hasFocusedKeyboardInput) {
+ document.body.classList.add("question-keyboard-open");
} else {
- document.body.classList.remove("question-input-open");
+ document.body.classList.remove("question-keyboard-open");
}
}
@@ -102,24 +101,52 @@ export function useQuestionInputFocusSync() {
useEffect(() => {
const handleFocusIn = (event: FocusEvent) => {
if (!isKeyboardInputTarget(event.target)) return;
- activeKeyboardInputCount += 1;
+ hasFocusedKeyboardInput = true;
syncQuestionInputOpenClass();
};
const handleFocusOut = (event: FocusEvent) => {
if (!isKeyboardInputTarget(event.target)) return;
- activeKeyboardInputCount = Math.max(0, activeKeyboardInputCount - 1);
+ window.setTimeout(() => {
+ hasFocusedKeyboardInput = isKeyboardInputTarget(document.activeElement);
+ syncQuestionInputOpenClass();
+ }, 0);
+ };
+
+ const unsubscribeConfig = viewPaddingsBridge.subscribeConfig((config) => {
+ if (config.keyboardHeight > 0) {
+ hasFocusedKeyboardInput = isKeyboardInputTarget(document.activeElement);
+ } else {
+ // Android Back can hide Flutter's keyboard without dispatching blur.
+ hasFocusedKeyboardInput = false;
+ }
syncQuestionInputOpenClass();
+ });
+
+ const initialViewportHeight = window.visualViewport?.height ?? 0;
+ const handleViewportResize = () => {
+ const viewportHeight = window.visualViewport?.height ?? 0;
+ if (
+ initialViewportHeight > 0 &&
+ viewportHeight >= initialViewportHeight - 40
+ ) {
+ hasFocusedKeyboardInput = false;
+ syncQuestionInputOpenClass();
+ }
};
document.addEventListener("focusin", handleFocusIn);
document.addEventListener("focusout", handleFocusOut);
+ window.visualViewport?.addEventListener("resize", handleViewportResize);
return () => {
document.removeEventListener("focusin", handleFocusIn);
document.removeEventListener("focusout", handleFocusOut);
- activeKeyboardInputCount = 0;
- if (activeSheetCount === 0) {
- document.body.classList.remove("question-input-open");
- }
+ window.visualViewport?.removeEventListener(
+ "resize",
+ handleViewportResize,
+ );
+ unsubscribeConfig();
+ hasFocusedKeyboardInput = false;
+ document.body.classList.remove("question-keyboard-open");
};
}, []);
}