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 { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useNotifications } from "../../../hooks/use-notifications";
|
import { useNotifications } from "../../../hooks/use-notifications";
|
||||||
|
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
||||||
import { NotificationItem } from "./notification-item";
|
import { NotificationItem } from "./notification-item";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export function NotificationCenter({
|
|||||||
setIsDropdownOpen,
|
setIsDropdownOpen,
|
||||||
} = useNotifications();
|
} = useNotifications();
|
||||||
|
|
||||||
|
const isMobile = useMobileViewport();
|
||||||
const bellRef = useRef<HTMLButtonElement>(null);
|
const bellRef = useRef<HTMLButtonElement>(null);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
||||||
@@ -30,11 +32,19 @@ export function NotificationCenter({
|
|||||||
const updatePosition = useCallback(() => {
|
const updatePosition = useCallback(() => {
|
||||||
if (!bellRef.current) return;
|
if (!bellRef.current) return;
|
||||||
const rect = bellRef.current.getBoundingClientRect();
|
const rect = bellRef.current.getBoundingClientRect();
|
||||||
setDropdownStyle({
|
if (isMobile) {
|
||||||
top: rect.bottom + 6,
|
setDropdownStyle({
|
||||||
right: window.innerWidth - rect.right,
|
top: rect.bottom + 6,
|
||||||
});
|
left: "1rem",
|
||||||
}, []);
|
right: "1rem",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDropdownStyle({
|
||||||
|
top: rect.bottom + 6,
|
||||||
|
right: window.innerWidth - rect.right,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDropdownOpen) return;
|
if (!isDropdownOpen) return;
|
||||||
@@ -104,55 +114,64 @@ export function NotificationCenter({
|
|||||||
|
|
||||||
{isDropdownOpen &&
|
{isDropdownOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<>
|
||||||
ref={dropdownRef}
|
{isMobile && (
|
||||||
role="dialog"
|
<div
|
||||||
aria-label="Notifications"
|
className="notification-dropdown-backdrop"
|
||||||
className="notification-dropdown"
|
onClick={() => setIsDropdownOpen(false)}
|
||||||
style={dropdownStyle}
|
aria-hidden="true"
|
||||||
>
|
/>
|
||||||
<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>,
|
<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,
|
document.body,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-2
@@ -4838,9 +4838,20 @@ a:active,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.notification-dropdown-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 9998;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-dropdown {
|
.notification-dropdown {
|
||||||
width: calc(100vw - 2rem);
|
width: auto;
|
||||||
max-width: 360px;
|
left: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
max-width: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-height: 70vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user