Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82db54ef2d | |||
| 6abb934b64 | |||
| 3c21eaaa99 | |||
| eee4fbcfa5 | |||
| fcd998390d | |||
| de33926a14 | |||
| e257dd38ad |
+3
-3
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user