Compare commits

...

7 Commits

Author SHA1 Message Date
Cyril 82db54ef2d fixup! 🐛(frontend) fix emoji image extension pPng to png 2026-03-03 12:07:31 +01:00
Cyril 6abb934b64 fixup! 🐛(frontend) fix emoji image extension pPng to png 2026-03-03 11:55:29 +01:00
Cyril 3c21eaaa99 🐛(frontend) fix emoji image extension pPng to png
fix reaction emoji display by correcting image file extension
2026-03-02 08:49:02 +01:00
Cyril eee4fbcfa5 fixup! (frontend) re-focus toolbar on shortcut when already open 2026-02-25 08:29:41 +01:00
Cyril fcd998390d (frontend) re-focus toolbar on shortcut when already open
allow keyboard shortcut to return focus to the emoji toolbar without closing it
2026-02-25 08:25:18 +01:00
Cyril de33926a14 fixup! ♻️(frontend) use react aria toolbar for arrow key navigation 2026-02-25 08:25:18 +01:00
Cyril e257dd38ad ♻️(frontend) use react aria toolbar for arrow key navigation
replace focus trap with roving tabindex following WAI-ARIA toolbar pattern
2026-02-25 08:25:00 +01:00
6 changed files with 148 additions and 70 deletions
+3 -3
View File
@@ -16,13 +16,13 @@ and this project adheres to
- ♻️(frontend) replace custom reactions toolbar with react aria popover #985
- 🔒️(frontend) uninstall curl from the frontend production image #987
- 💄(frontend) add focus ring to reaction emoji buttons
- 💄(frontend) add focus ring to reaction emoji buttons #1002
- ✨(frontend) introduce a shortcut settings tab #975
- 🚚(frontend) rename "wellknown" directory to "well-known" #1009
- 🌐(frontend) localize SR modifier labels #1010
- ⬆️(backend) update python dependencies #1011
- ♿️(a11y) fix focus ring on tab container components
- ♿️(frontend) fix focus ring on tab container components #1012
- ♻️(frontend) use react aria toolbar for arrow key navigation #1005
## [1.8.0] - 2026-02-20
@@ -12,11 +12,9 @@ import {
} from '@/features/rooms/livekit/components/ReactionPortal'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import {
Popover as RACPopover,
Dialog,
DialogTrigger,
} from 'react-aria-components'
import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
import { Popover as RACPopover, Toolbar } from 'react-aria-components'
import { FocusScope } from '@react-aria/focus'
import { Participant } from 'livekit-client'
import useRateLimiter from '@/hooks/useRateLimiter'
@@ -44,12 +42,20 @@ export const ReactionsToggle = () => {
const [reactions, setReactions] = useState<Reaction[]>([])
const instanceIdRef = useRef(0)
const room = useRoomContext()
const triggerRef = useRef<HTMLButtonElement>(null)
const firstEmojiButtonRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
useRegisterKeyboardShortcut({
id: 'reaction',
handler: () => setIsOpen((prev) => !prev),
handler: () => {
if (isOpen) {
firstEmojiButtonRef.current?.focus()
} else {
setIsOpen(true)
}
},
})
const sendReaction = async (emoji: string) => {
@@ -78,82 +84,146 @@ export const ReactionsToggle = () => {
}, ANIMATION_DURATION)
}
const focusNextToolbarControl = () => {
const nextContainer = document
.getElementById('reaction-toggle')
?.parentElement?.nextElementSibling
const focusable = nextContainer?.querySelector<HTMLElement>(
'button, [role="button"], [tabindex="0"]'
)
focusable?.focus()
}
const handleToolbarKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
triggerRef.current?.focus()
} else {
focusNextToolbarControl()
}
return
}
const buttons = Array.from(
(e.currentTarget as HTMLElement).querySelectorAll<HTMLElement>(
'[role="toolbar"] button'
)
)
if (buttons.length === 0) return
if (
e.key === 'ArrowRight' &&
document.activeElement === buttons[buttons.length - 1]
) {
e.preventDefault()
e.stopPropagation()
buttons[0].focus()
} else if (
e.key === 'ArrowLeft' &&
document.activeElement === buttons[0]
) {
e.preventDefault()
e.stopPropagation()
buttons[buttons.length - 1].focus()
}
}
const debouncedSendReaction = useRateLimiter({
callback: sendReaction,
maxCalls: 10,
windowMs: 1000,
})
const reactionShortcut = getShortcutDescriptorById('reaction')?.shortcut
const tooltipWithShortcut = t('tooltip', {
shortcut: formatShortcutLabel(reactionShortcut),
})
return (
<>
<div className={css({ position: 'relative' })}>
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<ToggleButton
square
variant="primaryDark"
aria-label={t('button')}
tooltip={t('button')}
isSelected={isOpen}
onChange={setIsOpen}
>
<RiEmotionLine />
</ToggleButton>
<RACPopover
placement="top"
offset={8}
isNonModal
shouldCloseOnInteractOutside={() => false}
className={css({
borderRadius: '8px',
padding: '0.35rem',
backgroundColor: 'primaryDark.50',
'&[data-entering]': {
animation: 'fade 200ms ease',
},
'&[data-exiting]': {
animation: 'fade 200ms ease-in reverse',
},
})}
>
<Dialog className={css({ outline: 'none' })}>
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */}
<FocusScope contain autoFocus restoreFocus>
<div
role="toolbar"
aria-orientation="horizontal"
<ToggleButton
ref={triggerRef}
id="reaction-toggle"
square
variant="primaryDark"
aria-label={t('button')}
aria-describedby="reactions-help"
aria-keyshortcuts="Control+Shift+E"
aria-expanded={isOpen}
aria-controls="reactions-toolbar"
tooltip={tooltipWithShortcut}
isSelected={isOpen}
onChange={setIsOpen}
>
<RiEmotionLine />
</ToggleButton>
<span id="reactions-help" className="sr-only">
{t('help')}
</span>
<RACPopover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
placement="top"
offset={8}
isNonModal
shouldCloseOnInteractOutside={() => false}
className={css({
borderRadius: '8px',
padding: '0.35rem',
backgroundColor: 'primaryDark.50',
'&[data-entering]': {
animation: 'fade 200ms ease',
},
'&[data-exiting]': {
animation: 'fade 200ms ease-in reverse',
},
})}
>
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */}
<FocusScope autoFocus restoreFocus>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- handles Tab exit and arrow wrapping for the portal-rendered toolbar */}
<div onKeyDownCapture={handleToolbarKeyDown}>
<div id="reactions-toolbar">
<Toolbar
orientation="horizontal"
aria-label={t('button')}
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
{Object.values(Emoji).map((emoji, index) => (
<Button
key={index}
onPress={() => debouncedSendReaction(emoji)}
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
variant="primaryTextDark"
size="sm"
square
data-attr={`send-reaction-${emoji}`}
>
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
width: '28px',
height: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
/>
</Button>
))}
</div>
</FocusScope>
</Dialog>
</RACPopover>
</DialogTrigger>
{Object.values(Emoji).map((emoji, index) => (
<Button
key={index}
ref={index === 0 ? firstEmojiButtonRef : undefined}
onPress={() => debouncedSendReaction(emoji)}
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
variant="primaryTextDark"
size="sm"
square
data-attr={`send-reaction-${emoji}`}
>
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
width: '28px',
height: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
/>
</Button>
))}
</Toolbar>
</div>
</div>
</FocusScope>
</RACPopover>
</div>
<ReactionPortals reactions={reactions} />
</>
+2
View File
@@ -211,6 +211,8 @@
"moreOptions": "Weitere Optionen",
"reactions": {
"button": "Reaktion senden",
"tooltip": "Reaktion senden ({{shortcut}})",
"help": "Pfeiltasten links und rechts zum Auswählen einer Reaktion. Escape zum Schließen. Strg+Umschalt+E zum Öffnen oder Zurückkehren zur Reaktionsleiste.",
"send": "Reaktion {{emoji}} senden",
"announce": "{{name}} : {{emoji}}",
"you": "Sie",
+2
View File
@@ -211,6 +211,8 @@
"moreOptions": "More options",
"reactions": {
"button": "Send reaction",
"tooltip": "Send a reaction ({{shortcut}})",
"help": "Use Left and Right arrow keys to choose a reaction. Press Escape to close. Press Control+Shift+E to open or return to the reactions bar.",
"send": "Send reaction {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "you",
+2
View File
@@ -211,6 +211,8 @@
"moreOptions": "Plus d'options",
"reactions": {
"button": "Envoyer une réaction",
"tooltip": "Envoyer une réaction ({{shortcut}})",
"help": "Flèches gauche/droite pour choisir une réaction. Échap pour fermer.",
"send": "Envoyer la réaction {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "vous",
+2
View File
@@ -211,6 +211,8 @@
"moreOptions": "Meer opties",
"reactions": {
"button": "Stuur reactie",
"tooltip": "Stuur een reactie ({{shortcut}})",
"help": "Gebruik de pijltjestoetsen links en rechts om een reactie te kiezen. Druk op Escape om te sluiten. Druk op Control+Shift+E om te openen of terug te keren naar de reactiebalk.",
"send": "Stuur reactie {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "U",