mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
fix(friendbar): auto-fit visible friend count so the bar stops clipping
The online-friends bar is portaled into the right toolbar nav, which sits inside `tb-nav-clip` (fixed, `max-w-[calc(50vw-242px)]`, `overflow-x: clip`). Each online friend adds a fixed `w-[132px]` chip, so the bar grew with every friend up to MAX_DISPLAY_COUNT (3). Once it exceeded the clip width the right edge was silently cut off - the scroll arrow and part of the search button disappeared. The portal slot is `shrink-0`, so the chips never compressed; they just overflowed and got clipped. Net effect: "more friends online = broken bar". Measure the room actually available between the bar's left edge and the viewport's right edge (re-measured on resize / ResizeObserver) and derive an effective visible count clamped 1..3, always reserving space for both arrows and the search chip so nothing clips at any width or friend count. The bar's left edge is stable (it follows fixed-width toolbar icons), so changing the chip count never moves it - no measurement feedback loop. Scroll offset now derives a clamped safeOffset used by every read, so a stale indexOffset after the list shrinks / the fit grows renders correctly and self-corrects on the next arrow click (no write-back effect).
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { FC, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { LocalizeText, MessengerFriend } from '../../../../api';
|
||||
import { FriendBarItemView } from './FriendBarItemView';
|
||||
import { motion, AnimatePresence, Variants } from 'framer-motion';
|
||||
|
||||
// Hard cap on simultaneously-shown friend chips. The effective count is
|
||||
// reduced below this when the bar would otherwise overflow its (clipped)
|
||||
// slot in the toolbar — see the width measurement below.
|
||||
const MAX_DISPLAY_COUNT = 3;
|
||||
|
||||
// Layout constants mirrored from FriendBarItemView / the flex gaps here, used
|
||||
// to compute how many friend chips fit in the available width. A "slot" is one
|
||||
// w-[132px] button plus the gap-[6px] that precedes it.
|
||||
const ITEM_SLOT = 138; // 132px chip + 6px gap (friend chip and search chip)
|
||||
const ARROWS_WIDTH = 52; // two w-[20px] arrows, each + 6px gap
|
||||
const REQUEST_SLOT = 120; // requests chip (only present when requestsCount > 0)
|
||||
const BASE_PAD = 8; // container px-[2px] + a little slack
|
||||
const RIGHT_SAFE = 24; // right inset (right-0/right-3) + pr-3 safety margin
|
||||
|
||||
// Mirrored from Toolbar to keep physics identical
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
@@ -23,9 +35,55 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
||||
{
|
||||
const { onlineFriends = [], requestsCount = 0 } = props;
|
||||
const [ indexOffset, setIndexOffset ] = useState(0);
|
||||
const [ maxVisible, setMaxVisible ] = useState(MAX_DISPLAY_COUNT);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const hasScrollableFriends = (onlineFriends.length > MAX_DISPLAY_COUNT);
|
||||
const visibleFriends = onlineFriends.slice(indexOffset, (indexOffset + MAX_DISPLAY_COUNT));
|
||||
|
||||
// Auto-fit the visible friend count to the room actually available between
|
||||
// the bar's left edge and the right side of the viewport. The bar lives in
|
||||
// a `overflow-x: clip` toolbar slot, so anything that doesn't fit would be
|
||||
// silently cut off (the scroll arrow / search button disappear). The bar's
|
||||
// left edge is stable (it sits after fixed-width toolbar icons), so growing
|
||||
// or shrinking the chip count never moves it — no measurement feedback loop.
|
||||
useLayoutEffect(() =>
|
||||
{
|
||||
const element = elementRef.current;
|
||||
|
||||
if(!element) return;
|
||||
|
||||
const measure = () =>
|
||||
{
|
||||
const left = element.getBoundingClientRect().left;
|
||||
const available = window.innerWidth - left - RIGHT_SAFE;
|
||||
const fixed = ARROWS_WIDTH + ITEM_SLOT /* search chip */ + BASE_PAD + ((requestsCount > 0) ? REQUEST_SLOT : 0);
|
||||
const fit = Math.floor((available - fixed) / ITEM_SLOT);
|
||||
const next = Math.max(1, Math.min(MAX_DISPLAY_COUNT, fit));
|
||||
|
||||
setMaxVisible(prev => ((prev === next) ? prev : next));
|
||||
};
|
||||
|
||||
measure();
|
||||
|
||||
const observer = new ResizeObserver(measure);
|
||||
|
||||
observer.observe(document.documentElement);
|
||||
window.addEventListener('resize', measure);
|
||||
|
||||
return () =>
|
||||
{
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', measure);
|
||||
};
|
||||
}, [ requestsCount, onlineFriends.length ]);
|
||||
|
||||
// `safeOffset` is the offset clamped to the current list/fit. Every read
|
||||
// below uses it, so a stale `indexOffset` (after the list shrinks or the fit
|
||||
// grows) renders correctly and self-corrects on the next arrow click — no
|
||||
// write-back effect needed.
|
||||
const maxOffset = Math.max(0, (onlineFriends.length - maxVisible));
|
||||
const safeOffset = Math.min(indexOffset, maxOffset);
|
||||
const canScrollLeft = (safeOffset > 0);
|
||||
const canScrollRight = (safeOffset < maxOffset);
|
||||
const visibleFriends = onlineFriends.slice(safeOffset, (safeOffset + maxVisible));
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -44,10 +102,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
||||
</motion.div> }
|
||||
<motion.div variants={itemVariants}>
|
||||
<div
|
||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ !canScrollLeft ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||
onClick={ () =>
|
||||
{
|
||||
if(indexOffset > 0) setIndexOffset(indexOffset - 1);
|
||||
if(canScrollLeft) setIndexOffset(safeOffset - 1);
|
||||
} }
|
||||
>
|
||||
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||
@@ -94,10 +152,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<div
|
||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ !canScrollRight ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||
onClick={ () =>
|
||||
{
|
||||
if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1);
|
||||
if(canScrollRight) setIndexOffset(safeOffset + 1);
|
||||
} }
|
||||
>
|
||||
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||
|
||||
Reference in New Issue
Block a user