diff --git a/CHANGELOG.md b/CHANGELOG.md index 205083d3..46dfe209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to - ♻️(fullstack) simplify source serialization - ✨(backend) expose room configuration to all API consumers +- 🩹(frontend) improve reaction toolbar centering with dynamic positioning ## [1.16.0] - 2026-05-13 diff --git a/src/frontend/src/features/layout/components/ControlBarRegion.tsx b/src/frontend/src/features/layout/components/ControlBarRegion.tsx index b19dd6ed..4550e5c0 100644 --- a/src/frontend/src/features/layout/components/ControlBarRegion.tsx +++ b/src/frontend/src/features/layout/components/ControlBarRegion.tsx @@ -22,6 +22,8 @@ const controlBarRegion = cva({ }, }) +export const CONTROL_BAR_REGION_ID = 'control-bar-region' + export type ControlBarRegionProps = React.HTMLAttributes & RecipeVariantProps @@ -34,6 +36,7 @@ export function ControlBarRegion({ return (
{ ?.focus() } +export const REACTIONS_TOGGLE_ID = 'reactions-toggle' + export const ReactionsToggle = () => { const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' }) @@ -35,7 +37,7 @@ export const ReactionsToggle = () => { return ( (null) const scrollRef = useRef(null) const { width } = useSize(scrollRef) @@ -74,6 +75,11 @@ export const ReactionButtonsContainer = ({ const [overflowing, setOverflowing] = useState(false) const [atStart, setAtStart] = useState(true) const [atEnd, setAtEnd] = useState(false) + const [ + shouldBeCenteredWithToggleButton, + setShouldBeCenteredWithToggleButton, + ] = useState(false) + const [rightOffset, setRightOffset] = useState(0) const updateArrows = useCallback(() => { const el = scrollRef.current @@ -83,6 +89,71 @@ export const ReactionButtonsContainer = ({ setAtEnd(el.scrollLeft + el.clientWidth >= el.scrollWidth - 1) }, []) + useEffect(() => { + if (isMobile) return + const region = document.getElementById(CONTROL_BAR_REGION_ID) + if (!region) return + const check = () => { + setShouldBeCenteredWithToggleButton( + window.innerWidth > region.clientWidth + ) + } + check() + const ro = new ResizeObserver(check) + ro.observe(region) + window.addEventListener('resize', check) + return () => { + ro.disconnect() + window.removeEventListener('resize', check) + } + }, [isMobile]) + + useLayoutEffect(() => { + if (!shouldBeCenteredWithToggleButton || isMobile) { + setRightOffset(0) + return + } + + const container = containerRef.current + if (!container) return + + let frame = 0 + + const align = () => { + const toggle = document.getElementById(REACTIONS_TOGGLE_ID) + if (!toggle) return + const toggleRect = toggle.getBoundingClientRect() + const containerRect = container.getBoundingClientRect() + const toggleCenterX = toggleRect.left + toggleRect.width / 2 + const containerCenterX = containerRect.left + containerRect.width / 2 + const shift = toggleCenterX - containerCenterX + if (Math.abs(shift) < 0.5) return + setRightOffset((prev) => prev - shift * 2) + } + + const schedule = () => { + cancelAnimationFrame(frame) + frame = requestAnimationFrame(align) + } + + schedule() + + const ro = new ResizeObserver(schedule) + ro.observe(container) + const region = document.getElementById(CONTROL_BAR_REGION_ID) + if (region) ro.observe(region) + const toggle = document.getElementById(REACTIONS_TOGGLE_ID) + if (toggle) ro.observe(toggle) + + window.addEventListener('resize', schedule) + + return () => { + cancelAnimationFrame(frame) + ro.disconnect() + window.removeEventListener('resize', schedule) + } + }, [shouldBeCenteredWithToggleButton, isMobile, isOpen]) + useEffect(() => { if (isOpen) { const id = requestAnimationFrame(() => setIsVisible(true)) @@ -101,9 +172,14 @@ export const ReactionButtonsContainer = ({ return ( {overflowing && (