🩹(frontend) improve reaction toolbar centering with dynamic positioning

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.
This commit is contained in:
lebaudantoine
2026-05-18 17:30:29 +02:00
committed by aleb_the_flash
parent b4ced74b1f
commit d47d13f041
4 changed files with 94 additions and 12 deletions
+1
View File
@@ -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
@@ -22,6 +22,8 @@ const controlBarRegion = cva({
},
})
export const CONTROL_BAR_REGION_ID = 'control-bar-region'
export type ControlBarRegionProps = React.HTMLAttributes<HTMLDivElement> &
RecipeVariantProps<typeof controlBarRegion>
@@ -34,6 +36,7 @@ export function ControlBarRegion({
return (
<div
role="region"
id={CONTROL_BAR_REGION_ID}
aria-label={t('controls.region')}
className={controlBarRegion({ mobile })}
{...props}
@@ -15,6 +15,8 @@ const focusReactionsToolbar = () => {
?.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 (
<ToggleButton
id="reactions-toggle"
id={REACTIONS_TOGGLE_ID}
data-attr="reactions-toggle"
square
variant="primaryDark"
@@ -1,4 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react'
import { styled } from '@/styled-system/jsx'
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
import { useIsMobile } from '@/utils/useIsMobile'
@@ -7,6 +13,9 @@ import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { Button } from '@/primitives'
import { CONTROL_BAR_REGION_ID } from '@/features/layout/components/ControlBarRegion'
import { REACTIONS_TOGGLE_ID } from '../ReactionsToggle'
const StyledContainer = styled('div', {
base: {
display: 'flex',
@@ -30,15 +39,6 @@ const StyledContainer = styled('div', {
pointerEvents: 'auto',
},
},
desktopOffset: {
true: {
// Ideally this value should be calculated dynamically in JavaScript to keep
// the reaction toolbar perfectly centered relative to the reaction toggle.
// However, for simplicity and to follow a pragmatic 80/20 approach,
// this value is currently hardcoded in CSS.
marginRight: '30px',
},
},
},
})
@@ -67,6 +67,7 @@ export const ReactionButtonsContainer = ({
const { isOpen } = useReactionsToolbar()
const isMobile = useIsMobile()
const containerRef = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(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 (
<StyledContainer
ref={containerRef}
aria-hidden={!isOpen}
isVisible={isVisible}
desktopOffset={!isMobile}
style={
shouldBeCenteredWithToggleButton && !isMobile
? { marginRight: `${rightOffset}px` }
: { margin: '0 15px' }
}
>
{overflowing && (
<Button