️(frontend) focus side panel container on open

Focus aside on side panel open so screen readers announce the aria-label
This commit is contained in:
Cyril
2026-06-29 11:48:46 +02:00
committed by aleb_the_flash
parent e910b1f0b7
commit c8d9d2fea8
2 changed files with 123 additions and 93 deletions
+1
View File
@@ -29,6 +29,7 @@ and this project adheres to
- 🩹(backend) identify externally provisioned users to PostHog - 🩹(backend) identify externally provisioned users to PostHog
- 🐛(backend) fix info panel crash for unregistered rooms - 🐛(backend) fix info panel crash for unregistered rooms
- ♿️(frontend) focus side panel container on open #1452
## [1.23.0] - 2026-07-08 ## [1.23.0] - 2026-07-08
@@ -6,8 +6,8 @@ import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react' import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList' import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel' import { PanelId, useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react' import React, { ReactNode, useCallback, useRef } from 'react'
import { Chat } from '../prefabs/Chat' import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects' import { Effects } from './effects/Effects'
import { Admin } from './Admin' import { Admin } from './Admin'
@@ -15,6 +15,7 @@ import { Tools } from './Tools'
import { Info } from './Info' import { Info } from './Info'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar' import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
type StyledSidePanelProps = { type StyledSidePanelProps = {
title: string title: string
@@ -29,103 +30,115 @@ type StyledSidePanelProps = {
isReactionToolbarOpen?: boolean isReactionToolbarOpen?: boolean
} }
const StyledSidePanel = ({ const StyledSidePanel = React.forwardRef<HTMLElement, StyledSidePanelProps>(
title, (
ariaLabel, {
children, title,
onClose, ariaLabel,
isClosed, children,
isReactionToolbarOpen, onClose,
closeButtonTooltip, isClosed,
isSubmenu = false, isReactionToolbarOpen,
onBack, closeButtonTooltip,
backButtonLabel, isSubmenu = false,
}: StyledSidePanelProps) => ( onBack,
<aside backButtonLabel,
className={css({ },
borderWidth: '1px', ref
borderStyle: 'solid', ) => (
borderColor: 'box.border', <aside
backgroundColor: 'box.bg', ref={ref}
color: 'box.text', tabIndex={-1}
borderRadius: 8, className={css({
flex: 1, borderWidth: '1px',
position: 'absolute', borderStyle: 'solid',
overflow: 'hidden', borderColor: 'box.border',
display: 'flex', backgroundColor: 'box.bg',
flexDirection: 'column', color: 'box.text',
margin: 'var(--sizes-room-side-panel-margin)', borderRadius: 8,
marginLeft: 0, flex: 1,
marginBottom: 0, position: 'absolute',
padding: 0, overflow: 'hidden',
gap: 0, display: 'flex',
right: 0, flexDirection: 'column',
top: 0, margin: 'var(--sizes-room-side-panel-margin)',
width: 'var(--sizes-room-side-panel)', marginLeft: 0,
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms', marginBottom: 0,
})} padding: 0,
style={{ gap: 0,
transform: isClosed right: 0,
? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))' top: 0,
: 'none', width: 'var(--sizes-room-side-panel)',
bottom: isReactionToolbarOpen transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))' '&:focus': {
: 'var(--sizes-room-control-bar)', outline: 'none',
}} },
aria-hidden={isClosed} })}
aria-label={ariaLabel} style={{
> transform: isClosed
<HStack alignItems="center"> ? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))'
{isSubmenu && ( : 'none',
<Button bottom: isReactionToolbarOpen
variant="secondaryText" ? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))'
size="sm" : 'var(--sizes-room-control-bar)',
square }}
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })} aria-hidden={isClosed}
aria-label={backButtonLabel} aria-label={ariaLabel}
onPress={onBack} >
<HStack alignItems="center">
{isSubmenu && (
<Button
variant="secondaryText"
size="sm"
square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel}
onPress={onBack}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
</Button>
)}
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
> >
<RiArrowLeftLine size={20} aria-hidden="true" /> {title}
</Button> </Heading>
)} </HStack>
<Heading <Div
slot="title" position="absolute"
level={1} top="5"
className={text({ variant: 'h2' })} right="5"
style={{ style={{
paddingLeft: isSubmenu ? 0 : '1.5rem', display: isClosed ? 'none' : undefined,
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}} }}
> >
{title} <Button
</Heading> invisible
</HStack> variant="tertiaryText"
<Div size="xs"
position="absolute" onPress={onClose}
top="5" aria-label={closeButtonTooltip}
right="5" tooltip={closeButtonTooltip}
style={{ >
display: isClosed ? 'none' : undefined, <RiCloseLine />
}} </Button>
> </Div>
<Button {children}
invisible </aside>
variant="tertiaryText" )
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
{children}
</aside>
) )
StyledSidePanel.displayName = 'StyledSidePanel'
type PanelProps = { type PanelProps = {
isOpen: boolean isOpen: boolean
children: React.ReactNode children: React.ReactNode
@@ -162,8 +175,24 @@ export const SidePanel = () => {
const { isOpen: isReactionToolbarOpen } = useReactionsToolbar() const { isOpen: isReactionToolbarOpen } = useReactionsToolbar()
const asideRef = useRef<HTMLElement>(null)
const panelManagesFocus = activePanelId === PanelId.CHAT
const focusAside = useCallback(() => {
requestAnimationFrame(() => {
asideRef.current?.focus({ preventScroll: true })
})
}, [])
useRestoreFocus(isSidePanelOpen && !panelManagesFocus, {
onOpened: focusAside,
preventScroll: true,
})
return ( return (
<StyledSidePanel <StyledSidePanel
ref={asideRef}
title={title} title={title}
ariaLabel={t('ariaLabel', { title })} ariaLabel={t('ariaLabel', { title })}
onClose={() => { onClose={() => {