Skip to content
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions src/components/Common/ResourceDefinitionCategoryPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Star,
X,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";

import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -159,6 +159,26 @@ export function ResourceDefinitionCategoryPicker<T>({
);
const [searchQuery, setSearchQuery] = useState("");
const [breadcrumbsExpanded, setBreadcrumbsExpanded] = useState(false);
// Set on touch/pen pointerup to avoid double selection from the following onSelect.
const pointerSelectedRef = useRef(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
// Records where a touch/pen press started so a moved pointer (scroll) is not
// treated as a tap on pointerup.
const pointerStartRef = useRef<{ x: number; y: number } | null>(null);

const handleItemPointerDown = (e: React.PointerEvent) => {
pointerStartRef.current =
e.pointerType === "mouse" ? null : { x: e.clientX, y: e.clientY };
};

// True only for a touch/pen release that stayed within the tap threshold.
const isPointerTap = (e: React.PointerEvent) => {
const start = pointerStartRef.current;
pointerStartRef.current = null;
if (!start) return false;
return (
Math.abs(e.clientX - start.x) <= 10 && Math.abs(e.clientY - start.y) <= 10
);
};
Comment thread
navaspavil marked this conversation as resolved.
Outdated

// Sync open state with defaultOpen prop for controlled auto-open behavior
useEffect(() => {
Expand Down Expand Up @@ -308,6 +328,17 @@ export function ResourceDefinitionCategoryPicker<T>({
// Reset search when navigating
const resetSearch = () => setSearchQuery("");

// On mobile, blur the focused search input when the list is scrolled. While it
// stays focused, vaul re-sizes the drawer to the keyboard on every
// visualViewport event fired during scroll, which shows up as a blink.
const dismissKeyboardOnScroll = () => {
if (!isMobile) return;
const active = document.activeElement;
if (active instanceof HTMLElement && active.tagName === "INPUT") {
active.blur();
}
};

const handleCategorySelect = (
categorySlug: string,
categoryTitle: string,
Expand Down Expand Up @@ -360,6 +391,11 @@ export function ResourceDefinitionCategoryPicker<T>({
}
} else {
onValueChange(definition as T);
// Blur the focused search input first so the mobile keyboard dismisses
// cleanly instead of the page scrolling to the input as the drawer closes.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
setOpen(false);
resetSearch();
}
Expand Down Expand Up @@ -580,9 +616,20 @@ export function ResourceDefinitionCategoryPicker<T>({
<CommandItem
key={category.id}
value={category.title}
onSelect={() =>
handleCategorySelect(category.slug, category.title)
}
onSelect={() => {
if (pointerSelectedRef.current) {
pointerSelectedRef.current = false;
return;
}
handleCategorySelect(category.slug, category.title);
}}
onPointerDown={handleItemPointerDown}
onPointerUp={(e) => {
if (isPointerTap(e)) {
pointerSelectedRef.current = true;
handleCategorySelect(category.slug, category.title);
}
}}
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 hover:text-gray-900 transition-colors duration-150 border-b border-gray-200"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
Expand All @@ -609,7 +656,20 @@ export function ResourceDefinitionCategoryPicker<T>({
<CommandItem
key={category.id}
value={category.title}
onSelect={() => handleCategorySelect(category.slug, category.title)}
onSelect={() => {
if (pointerSelectedRef.current) {
pointerSelectedRef.current = false;
return;
}
handleCategorySelect(category.slug, category.title);
}}
onPointerDown={handleItemPointerDown}
onPointerUp={(e) => {
if (isPointerTap(e)) {
pointerSelectedRef.current = true;
handleCategorySelect(category.slug, category.title);
}
}}
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 hover:text-gray-900 transition-colors duration-150 border-b border-gray-200"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
Expand Down Expand Up @@ -639,7 +699,22 @@ export function ResourceDefinitionCategoryPicker<T>({
<CommandItem
key={definition.id}
value={`${definition.title}-${definition.id}`}
onSelect={() => handleDefinitionSelect(definition)}
onSelect={() => {
if (pointerSelectedRef.current) {
pointerSelectedRef.current = false;
return;
}
handleDefinitionSelect(definition);
}}
onPointerDown={handleItemPointerDown}
onPointerUp={(e) => {
// Ignore taps on the favorite toggle button
if (e.target instanceof Element && e.target.closest("button")) return;
if (isPointerTap(e)) {
pointerSelectedRef.current = true;
handleDefinitionSelect(definition);
}
}}
className={cn(
"flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 hover:text-gray-900 transition-colors duration-150 border-b border-gray-200 last:border-b-0",
searchQuery && definition.category && "py-1",
Expand Down Expand Up @@ -688,6 +763,7 @@ export function ResourceDefinitionCategoryPicker<T>({

const renderRecentItems = () => (
<div
data-vaul-no-drag
className={cn(
"overflow-auto min-h-0",
isMobile ? "max-h-full" : "max-h-[40vh]",
Expand Down Expand Up @@ -726,6 +802,7 @@ export function ResourceDefinitionCategoryPicker<T>({

const renderFavoriteItems = () => (
<div
data-vaul-no-drag
className={cn(
"overflow-auto min-h-0",
isMobile ? "max-h-full" : "max-h-[40vh]",
Expand Down Expand Up @@ -776,6 +853,8 @@ export function ResourceDefinitionCategoryPicker<T>({
{renderSearchInput()}
{renderBreadcrumbs()}
<CommandList
data-vaul-no-drag
onScroll={dismissKeyboardOnScroll}
className={cn(isMobile ? "max-h-full h-[40vh]" : "max-h-[40vh]")}
>
{renderEmptyState()}
Expand Down
Loading