️(frontend) improve accessibility and navigation for reaction toolbar

Refine keyboard behavior so left and right arrow keys no longer
control toolbar navigation, allowing users to directly tab into
individual reaction buttons for faster and more intuitive access.

Remove the reaction toolbar from accessibility tree to avoid
unnecessary focus and announcements, keeping assistive technology
focused on the actual reaction controls.

This ensures the UX centers on reaction actions rather than
auxiliary navigation elements.

Also, not related to this topic, I've reworked the scroll viewport styles.
This commit is contained in:
lebaudantoine
2026-05-18 18:02:44 +02:00
committed by aleb_the_flash
parent 318447f2b3
commit eb74feaa0d
2 changed files with 53 additions and 46 deletions
@@ -8,10 +8,11 @@ import {
import { styled } from '@/styled-system/jsx'
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
import { useIsMobile } from '@/utils/useIsMobile'
import { css } from '@/styled-system/css'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { Button } from '@/primitives'
import { ReactionsKeyboardNavigation } from './ReactionsKeyboardNavigation'
import { FocusScope } from '@react-aria/focus'
import { CONTROL_BAR_REGION_ID } from '@/features/layout/components/ControlBarRegion'
import { REACTIONS_TOGGLE_ID } from '../ReactionsToggle'
@@ -42,18 +43,20 @@ const StyledContainer = styled('div', {
},
})
const scrollViewport = css({
display: 'flex',
gap: '0.2rem',
overflowX: 'auto',
padding: '0.19rem',
scrollBehavior: 'smooth',
minWidth: 0,
flex: '1 1 auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
'& > *': {
flexShrink: 0,
const StyledScrollViewport = styled('div', {
base: {
display: 'flex',
gap: '0.2rem',
overflowX: 'auto',
padding: '0.19rem',
scrollBehavior: 'smooth',
minWidth: 0,
flex: '1 1 auto',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
'& > *': {
flexShrink: 0,
},
},
})
@@ -182,29 +185,40 @@ export const ReactionButtonsContainer = ({
}
>
{overflowing && (
<Button
onPress={() => scrollBy(-SCROLL_AMOUNT)}
variant="primaryTextDark"
size="sm"
isDisabled={atStart}
round
>
<RiArrowLeftSLine />
</Button>
<div aria-hidden="true">
<Button
onPress={() => scrollBy(-SCROLL_AMOUNT)}
variant="primaryTextDark"
size="sm"
isDisabled={atStart}
round
excludeFromTabOrder
>
<RiArrowLeftSLine />
</Button>
</div>
)}
<div ref={scrollRef} className={scrollViewport} onScroll={updateArrows}>
{children}
</div>
{/* eslint-disable-next-line jsx-a11y/no-autofocus*/}
<FocusScope autoFocus>
<ReactionsKeyboardNavigation>
<StyledScrollViewport ref={scrollRef} onScroll={updateArrows}>
{children}
</StyledScrollViewport>
</ReactionsKeyboardNavigation>
</FocusScope>
{overflowing && (
<Button
onPress={() => scrollBy(SCROLL_AMOUNT)}
variant="primaryTextDark"
size="sm"
isDisabled={atEnd}
round
>
<RiArrowRightSLine />
</Button>
<div aria-hidden="true">
<Button
onPress={() => scrollBy(SCROLL_AMOUNT)}
variant="primaryTextDark"
size="sm"
isDisabled={atEnd}
round
excludeFromTabOrder
>
<RiArrowRightSLine />
</Button>
</div>
)}
</StyledContainer>
)
@@ -1,10 +1,8 @@
import { FocusScope } from '@react-aria/focus'
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
import { ReactionButton } from './ReactionButton'
import { Emoji } from '../../types'
import { styled } from '@/styled-system/jsx'
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
import { ReactionsKeyboardNavigation } from './ReactionsKeyboardNavigation'
import { ReactionButtonsContainer } from './ReactionButtonsContainer'
const Container = styled('div', {
@@ -27,16 +25,11 @@ export const ReactionsToolbar = () => {
return (
<Container>
{/* eslint-disable-next-line jsx-a11y/no-autofocus*/}
<FocusScope autoFocus>
<ReactionsKeyboardNavigation>
<ReactionButtonsContainer>
{Object.values(Emoji).map((emoji) => (
<ReactionButton key={emoji} emoji={emoji} />
))}
</ReactionButtonsContainer>
</ReactionsKeyboardNavigation>
</FocusScope>
<ReactionButtonsContainer>
{Object.values(Emoji).map((emoji) => (
<ReactionButton key={emoji} emoji={emoji} />
))}
</ReactionButtonsContainer>
</Container>
)
}