feat: make notification center mobile friendly
- Use useMobileViewport to detect mobile and position dropdown centered with left/right margins instead of right-aligned, which caused overflow on small screens. - Add a semi-transparent backdrop overlay on mobile so tapping outside the dropdown naturally closes it. - Update mobile CSS: notification-dropdown fills screen width with 0.75rem margins, max-height capped at 70vh for reachability. - Remove the 360px max-width cap on mobile so the dropdown uses available screen space properly. Quality gates: tsc --noEmit (pass), build (pass)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNotifications } from "../../../hooks/use-notifications";
|
||||
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
||||
import { NotificationItem } from "./notification-item";
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
@@ -23,6 +24,7 @@ export function NotificationCenter({
|
||||
setIsDropdownOpen,
|
||||
} = useNotifications();
|
||||
|
||||
const isMobile = useMobileViewport();
|
||||
const bellRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
||||
@@ -30,11 +32,19 @@ export function NotificationCenter({
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!bellRef.current) return;
|
||||
const rect = bellRef.current.getBoundingClientRect();
|
||||
setDropdownStyle({
|
||||
top: rect.bottom + 6,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}, []);
|
||||
if (isMobile) {
|
||||
setDropdownStyle({
|
||||
top: rect.bottom + 6,
|
||||
left: "1rem",
|
||||
right: "1rem",
|
||||
});
|
||||
} else {
|
||||
setDropdownStyle({
|
||||
top: rect.bottom + 6,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDropdownOpen) return;
|
||||
@@ -104,55 +114,64 @@ export function NotificationCenter({
|
||||
|
||||
{isDropdownOpen &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="dialog"
|
||||
aria-label="Notifications"
|
||||
className="notification-dropdown"
|
||||
style={dropdownStyle}
|
||||
>
|
||||
<div className="notification-dropdown-header">
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
|
||||
<ul className="notification-list">
|
||||
{notifications.length === 0 ? (
|
||||
<li className="notification-empty">No notifications</li>
|
||||
) : (
|
||||
notifications.map((n) => (
|
||||
<NotificationItem
|
||||
key={n.id}
|
||||
notification={n}
|
||||
onMarkRead={markRead}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{notifications.length > 0 && (
|
||||
<div className="notification-dropdown-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="notification-mark-all"
|
||||
onClick={() => {
|
||||
void markAllRead();
|
||||
}}
|
||||
>
|
||||
Mark all as read
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="notification-clear-all"
|
||||
onClick={() => {
|
||||
void clearAll();
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
{isMobile && (
|
||||
<div
|
||||
className="notification-dropdown-backdrop"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="dialog"
|
||||
aria-label="Notifications"
|
||||
className="notification-dropdown"
|
||||
style={dropdownStyle}
|
||||
>
|
||||
<div className="notification-dropdown-header">
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
|
||||
<ul className="notification-list">
|
||||
{notifications.length === 0 ? (
|
||||
<li className="notification-empty">No notifications</li>
|
||||
) : (
|
||||
notifications.map((n) => (
|
||||
<NotificationItem
|
||||
key={n.id}
|
||||
notification={n}
|
||||
onMarkRead={markRead}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{notifications.length > 0 && (
|
||||
<div className="notification-dropdown-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="notification-mark-all"
|
||||
onClick={() => {
|
||||
void markAllRead();
|
||||
}}
|
||||
>
|
||||
Mark all as read
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="notification-clear-all"
|
||||
onClick={() => {
|
||||
void clearAll();
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
|
||||
+13
-2
@@ -4838,9 +4838,20 @@ a:active,
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.notification-dropdown-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
max-width: 360px;
|
||||
width: auto;
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
max-width: none;
|
||||
border-radius: 12px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user