fix(ui): anchor slash menu to bottom edge when flipped above cursor
- initialize position state with a `bottom` field set to null - use `bottom` css property instead of `top` when menu flips above the anchor to prevent floating when content shrinks on query filtering - remove `items.length` from layout effect dependencies since repositioning on item count change caused the flipping issue - build `positionStyle` object conditionally based on whether `bottom` is set
This commit is contained in:
@@ -66,7 +66,7 @@ export default function SlashMenu({
|
|||||||
}, [allowed, query]);
|
}, [allowed, query]);
|
||||||
|
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
const [position, setPosition] = useState({ top: 0, left: 0, maxHeight: MENU_MAX_HEIGHT });
|
const [position, setPosition] = useState({ top: 0, bottom: null, left: 0, maxHeight: MENU_MAX_HEIGHT });
|
||||||
|
|
||||||
// Scroll l'élément sélectionné dans la vue (interne au menu uniquement)
|
// Scroll l'élément sélectionné dans la vue (interne au menu uniquement)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -76,6 +76,9 @@ export default function SlashMenu({
|
|||||||
|
|
||||||
// Positionnement adaptatif : flip au-dessus si pas assez de place en bas,
|
// Positionnement adaptatif : flip au-dessus si pas assez de place en bas,
|
||||||
// clamp horizontalement, et limite la hauteur à l'espace disponible.
|
// clamp horizontalement, et limite la hauteur à l'espace disponible.
|
||||||
|
// Quand on est en mode « au-dessus », on ancre via `bottom` plutôt que
|
||||||
|
// `top` — sinon, quand le contenu rétrécit (filtrage par query), le bas
|
||||||
|
// de la boîte décolle du champ et flotte en l'air.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!anchorRect || typeof window === 'undefined') return;
|
if (!anchorRect || typeof window === 'undefined') return;
|
||||||
const vh = window.innerHeight;
|
const vh = window.innerHeight;
|
||||||
@@ -83,14 +86,15 @@ export default function SlashMenu({
|
|||||||
const spaceBelow = vh - anchorRect.bottom - VIEWPORT_MARGIN;
|
const spaceBelow = vh - anchorRect.bottom - VIEWPORT_MARGIN;
|
||||||
const spaceAbove = anchorRect.top - VIEWPORT_MARGIN;
|
const spaceAbove = anchorRect.top - VIEWPORT_MARGIN;
|
||||||
|
|
||||||
let top;
|
let top = null;
|
||||||
|
let bottom = null;
|
||||||
let maxHeight;
|
let maxHeight;
|
||||||
if (spaceBelow >= Math.min(MENU_MAX_HEIGHT, 200) || spaceBelow >= spaceAbove) {
|
if (spaceBelow >= Math.min(MENU_MAX_HEIGHT, 200) || spaceBelow >= spaceAbove) {
|
||||||
top = anchorRect.bottom + 6;
|
top = anchorRect.bottom + 6;
|
||||||
maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceBelow - 6));
|
maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceBelow - 6));
|
||||||
} else {
|
} else {
|
||||||
|
bottom = vh - anchorRect.top + 6;
|
||||||
maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceAbove - 6));
|
maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceAbove - 6));
|
||||||
top = anchorRect.top - 6 - maxHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let left = anchorRect.left;
|
let left = anchorRect.left;
|
||||||
@@ -99,18 +103,21 @@ export default function SlashMenu({
|
|||||||
}
|
}
|
||||||
if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
|
if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
|
||||||
|
|
||||||
setPosition({ top, left, maxHeight });
|
setPosition({ top, bottom, left, maxHeight });
|
||||||
}, [anchorRect, items.length]);
|
}, [anchorRect]);
|
||||||
|
|
||||||
if (!anchorRect) return null;
|
if (!anchorRect) return null;
|
||||||
|
|
||||||
const { top, left, maxHeight } = position;
|
const { top, bottom, left, maxHeight } = position;
|
||||||
|
const positionStyle = bottom != null
|
||||||
|
? { bottom, left, width: MENU_WIDTH, maxHeight }
|
||||||
|
: { top, left, width: MENU_WIDTH, maxHeight };
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed z-50 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-3 py-2 text-xs text-neutral-500"
|
className="fixed z-50 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-3 py-2 text-xs text-neutral-500"
|
||||||
style={{ top, left, width: MENU_WIDTH, maxHeight }}
|
style={positionStyle}
|
||||||
>
|
>
|
||||||
Aucune commande pour « {query} »
|
Aucune commande pour « {query} »
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +129,7 @@ export default function SlashMenu({
|
|||||||
ref={listRef}
|
ref={listRef}
|
||||||
data-slash-menu
|
data-slash-menu
|
||||||
className="fixed z-50 overflow-y-auto rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md py-1"
|
className="fixed z-50 overflow-y-auto rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md py-1"
|
||||||
style={{ top, left, width: MENU_WIDTH, maxHeight }}
|
style={positionStyle}
|
||||||
onMouseDown={(e) => e.preventDefault()} // ne pas voler le focus
|
onMouseDown={(e) => e.preventDefault()} // ne pas voler le focus
|
||||||
>
|
>
|
||||||
<div className="px-3 pt-1.5 pb-1 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
|
<div className="px-3 pt-1.5 pb-1 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user