From d47d13f04141df2e2ad4a73a7df75c2980efcc9e Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 18 May 2026 17:30:29 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9(frontend)=20improve=20reaction=20t?= =?UTF-8?q?oolbar=20centering=20with=20dynamic=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the previous hardcoded offset approach with a dedicated hook that dynamically centers the reaction toolbar relative to the reaction toggle. This improves layout accuracy and removes reliance on fixed values. Note: ResizeObserver is used for positioning updates and may not be supported in older browsers. Additional testing will be performed before production release. Replace direct DOM queries using hardcoded IDs with proper constants. --- CHANGELOG.md | 1 + .../layout/components/ControlBarRegion.tsx | 3 + .../reactions/components/ReactionsToggle.tsx | 4 +- .../toolbar/ReactionButtonsContainer.tsx | 98 ++++++++++++++++--- 4 files changed, 94 insertions(+), 12 deletions(-) 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 && (