fix(rtl): flip JS-positioned popovers (storage, logout, account switcher, calendar picker)
These popovers are portaled and positioned via inline styles computed from getBoundingClientRect() rather than Tailwind classes, so the logical start-0/end-0 fix doesn't reach them. They always anchored to the physical right of their trigger (rect.right + 8), which in RTL pushes them further into the edge the trigger is already flush against instead of toward the visible content area. Added isDocumentRTL() to i18n/direction.ts and used it to mirror the computed position in: - navigation-rail.tsx: storage quota popover, logout/switch-account menu - account-switcher.tsx: both the rail and expanded-sidebar variants - calendar-invitation-banner.tsx: the "add to calendar" picker
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useTranslations, useFormatter } from 'next-intl';
|
||||
import { useRouter } from '@/i18n/navigation';
|
||||
import { isDocumentRTL } from '@/i18n/direction';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useCalendarStore } from '@/stores/calendar-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
@@ -374,7 +375,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [showCalendarPicker, setShowCalendarPicker] = useState(false);
|
||||
const [pickerPosition, setPickerPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
const [pickerPosition, setPickerPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
|
||||
const pickerTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>('');
|
||||
const [rawIcsMethod, setRawIcsMethod] = useState<InvitationMethod>('unknown');
|
||||
@@ -1026,7 +1027,11 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
}
|
||||
if (pickerTriggerRef.current) {
|
||||
const rect = pickerTriggerRef.current.getBoundingClientRect();
|
||||
setPickerPosition({ top: rect.bottom + 4, left: rect.left });
|
||||
setPickerPosition(
|
||||
isDocumentRTL()
|
||||
? { top: rect.bottom + 4, right: window.innerWidth - rect.right }
|
||||
: { top: rect.bottom + 4, left: rect.left }
|
||||
);
|
||||
}
|
||||
setShowCalendarPicker(true);
|
||||
}}
|
||||
@@ -1041,7 +1046,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
{showCalendarPicker && calendars.length > 1 && pickerPosition && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
className="fixed w-52 bg-background rounded-lg shadow-lg border border-border z-50 py-1"
|
||||
style={{ top: pickerPosition.top, left: pickerPosition.left }}
|
||||
style={{ top: pickerPosition.top, left: pickerPosition.left, right: pickerPosition.right }}
|
||||
>
|
||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{t('select_calendar')}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
|
||||
import { useAccountStore, type AccountEntry } from "@/stores/account-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds } from "@/lib/account-utils";
|
||||
import { isDocumentRTL } from "@/i18n/direction";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
@@ -54,18 +55,35 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current) return;
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const rtl = isDocumentRTL();
|
||||
if (variant === "rail") {
|
||||
setPopoverStyle({
|
||||
setPopoverStyle(
|
||||
rtl
|
||||
? {
|
||||
position: "fixed",
|
||||
right: window.innerWidth - rect.left + 8,
|
||||
bottom: Math.max(8, window.innerHeight - rect.bottom),
|
||||
}
|
||||
: {
|
||||
position: "fixed",
|
||||
left: rect.right + 8,
|
||||
bottom: Math.max(8, window.innerHeight - rect.bottom),
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setPopoverStyle({
|
||||
setPopoverStyle(
|
||||
rtl
|
||||
? {
|
||||
position: "fixed",
|
||||
right: window.innerWidth - rect.right,
|
||||
top: rect.bottom + 4,
|
||||
}
|
||||
: {
|
||||
position: "fixed",
|
||||
left: rect.left,
|
||||
top: rect.bottom + 4,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [variant]);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useAccountStore } from "@/stores/account-store";
|
||||
import { useUpdateStore, selectHasUpdate } from "@/stores/update-store";
|
||||
import { getActiveAccountSlotHeaders } from "@/lib/auth/active-account-slot";
|
||||
import { getMaxAccounts } from "@/lib/account-utils";
|
||||
import { isDocumentRTL } from "@/i18n/direction";
|
||||
import { cn, formatFileSize } from "@/lib/utils";
|
||||
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
||||
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
||||
@@ -70,11 +71,19 @@ function StorageQuotaCircle({ quota, usagePercent }: { quota: { used: number; to
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current) return;
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPopoverStyle({
|
||||
setPopoverStyle(
|
||||
isDocumentRTL()
|
||||
? {
|
||||
position: "fixed",
|
||||
right: window.innerWidth - rect.left + 8,
|
||||
bottom: window.innerHeight - rect.bottom,
|
||||
}
|
||||
: {
|
||||
position: "fixed",
|
||||
left: rect.right + 8,
|
||||
bottom: window.innerHeight - rect.bottom,
|
||||
});
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,11 +227,19 @@ export function NavigationRail({
|
||||
const updateLogoutPosition = useCallback(() => {
|
||||
if (!logoutBtnRef.current) return;
|
||||
const rect = logoutBtnRef.current.getBoundingClientRect();
|
||||
setLogoutPopoverStyle({
|
||||
setLogoutPopoverStyle(
|
||||
isDocumentRTL()
|
||||
? {
|
||||
position: "fixed",
|
||||
right: window.innerWidth - rect.left + 8,
|
||||
bottom: Math.max(8, window.innerHeight - rect.bottom),
|
||||
}
|
||||
: {
|
||||
position: "fixed",
|
||||
left: rect.right + 8,
|
||||
bottom: Math.max(8, window.innerHeight - rect.bottom),
|
||||
});
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,3 +4,13 @@ const rtlLocales = new Set(['ar', 'he', 'fa']);
|
||||
export function getLocaleDirection(locale: string): 'ltr' | 'rtl' {
|
||||
return rtlLocales.has(locale) ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the document is currently rendering right-to-left. For popovers
|
||||
* positioned in JS via getBoundingClientRect() (Tailwind's logical start-0/
|
||||
* end-0 utilities don't apply to inline fixed-position styles), check this
|
||||
* to anchor on the correct physical side.
|
||||
*/
|
||||
export function isDocumentRTL(): boolean {
|
||||
return typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user