Compare commits

...

4 Commits

Author SHA1 Message Date
Roee Sivan 531774fbda 🐛(frontend) keep the admin panel in place when changing a setting
React Aria renders the radio and switch inputs visually hidden and
absolutely positioned. Their labels were not positioned, so in the
side panel the inputs were anchored to the aside and did not scroll
with its content. Focusing one made Chrome scroll the aside, which
has no scrollbar, hiding the panel content.

Position the labels so each hidden input stays inside its control.
2026-10-11 16:08:25 +02:00
Ovgodd 86d97b427c 🐛(frontend) play the speaker test when no output is picked
The speaker test called setSinkId('default'). Firefox rejects it
and playback stopped. Skip that id and play on the system output.
2026-10-11 00:16:47 +02:00
Ovgodd b0bb5bc61b 🐛(frontend) omit the default speaker id sent to LiveKit
Firefox rejects setSinkId('default') when a remote audio track
is attached. Omit that id and only send a real device id.
2026-10-11 00:16:47 +02:00
Ovgodd 3773e43d93 ♿️(frontend) show a visible focus outline on menu items
Keyboard focus was cleared with outline:none.
2026-10-09 18:13:14 +02:00
7 changed files with 38 additions and 8 deletions
+4
View File
@@ -18,6 +18,7 @@ and this project adheres to
- ⚡️(backend) reduce domain queries on the application token endpoint - ⚡️(backend) reduce domain queries on the application token endpoint
- ♻️️️(backend) use a dedicated auth scheme for LiveKit token auth - ♻️️️(backend) use a dedicated auth scheme for LiveKit token auth
- ♻️(all) stop relying on cookies for the lobby flow - ♻️(all) stop relying on cookies for the lobby flow
- ♿️(frontend) show a visible focus outline on menu items #1797
### Fixed ### Fixed
@@ -25,6 +26,9 @@ and this project adheres to
- 🔒️(frontend) upgrade tiff to fix CVE-2026-4775 - 🔒️(frontend) upgrade tiff to fix CVE-2026-4775
- 🔒️(backend) bind accepted lobby entries to the current username - 🔒️(backend) bind accepted lobby entries to the current username
- 🔒️(backend) sign lobby participant id - 🔒️(backend) sign lobby participant id
- 🐛(frontend) omit the default speaker id sent to LiveKit
- 🐛(frontend) play the speaker test when no output is picked
- 🐛(frontend) keep the admin panel in place when changing a setting #1804
## [1.34.0] - 2026-10-07 ## [1.34.0] - 2026-10-07
@@ -45,6 +45,7 @@ import { userStore } from '@/stores/user'
import { WatchMediaDeviceErrors } from './WatchMediaDeviceErrors' import { WatchMediaDeviceErrors } from './WatchMediaDeviceErrors'
import { MeetDevtools } from '@/features/devtools' import { MeetDevtools } from '@/features/devtools'
import { VOICE_AUDIO_CONSTRAINTS } from '@/features/rooms/livekit/utils/constants' import { VOICE_AUDIO_CONSTRAINTS } from '@/features/rooms/livekit/utils/constants'
import { resolveAudioOutputDeviceId } from '@/features/rooms/utils/resolveAudioOutputDeviceId'
export const Conference = ({ export const Conference = ({
roomId, roomId,
@@ -120,7 +121,7 @@ export const Conference = ({
...VOICE_AUDIO_CONSTRAINTS, ...VOICE_AUDIO_CONSTRAINTS,
}, },
audioOutput: { audioOutput: {
deviceId: userConfig.audioOutputDeviceId ?? undefined, deviceId: resolveAudioOutputDeviceId(userConfig.audioOutputDeviceId),
}, },
} }
// do not rely on the userConfig object directly as its reference may change on every render // do not rely on the userConfig object directly as its reference may change on every render
@@ -4,6 +4,7 @@ import { RiVolumeUpLine } from '@remixicon/react'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { Button } from '@/primitives' import { Button } from '@/primitives'
import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput' import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput'
import { resolveAudioOutputDeviceId } from '@/features/rooms/utils/resolveAudioOutputDeviceId'
// Speaker test in the audiooutput menu footer (Meet-style UX). Outputs have // Speaker test in the audiooutput menu footer (Meet-style UX). Outputs have
// no track: the test plays a bundled file through the selected sink, and // no track: the test plays a bundled file through the selected sink, and
@@ -66,6 +67,8 @@ export const OutputSoundTester = ({
const audioRef = useRef<HTMLAudioElement>(null) const audioRef = useRef<HTMLAudioElement>(null)
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const outputId = resolveAudioOutputDeviceId(sinkId)
const latestSinkIdRef = useRef(sinkId) const latestSinkIdRef = useRef(sinkId)
latestSinkIdRef.current = sinkId latestSinkIdRef.current = sinkId
@@ -79,15 +82,15 @@ export const OutputSoundTester = ({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!sinkId || !canTestAudioOutput()) return if (!outputId || !canTestAudioOutput()) return
audioRef.current?.setSinkId(sinkId).catch(() => { audioRef.current?.setSinkId(outputId).catch(() => {
// Re-routing failed (stale or unplugged device): stop the test rather // Re-routing failed (stale or unplugged device): stop the test rather
// than keep playing through the previous sink. // than keep playing through the previous sink.
if (latestSinkIdRef.current === sinkId) { if (latestSinkIdRef.current === sinkId) {
stopPlayback() stopPlayback()
} }
}) })
}, [sinkId, stopPlayback]) }, [outputId, sinkId, stopPlayback])
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current
@@ -108,8 +111,8 @@ export const OutputSoundTester = ({
// Confirm routing before starting: a no-op when already routed, // Confirm routing before starting: a no-op when already routed,
// but rejects on a stale device id, so the test never plays // but rejects on a stale device id, so the test never plays
// through the wrong sink. // through the wrong sink.
if (sinkId && canTestAudioOutput()) { if (outputId && canTestAudioOutput()) {
await audio.setSinkId(sinkId) await audio.setSinkId(outputId)
} }
await audio.play() await audio.play()
setIsPlaying(true) setIsPlaying(true)
@@ -0,0 +1,11 @@
/**
* `'default'` means "use the system output". Firefox has no device with that
* id, so setSinkId('default') rejects. Omit it and the browser keeps the OS
* default, which is the same result.
*/
export const resolveAudioOutputDeviceId = (
deviceId: string | undefined
): string | undefined => {
if (!deviceId || deviceId === 'default') return undefined
return deviceId
}
+2
View File
@@ -9,6 +9,8 @@ import { type StyledVariantProps } from '@/styled-system/types'
const StyledRadio = styled(RACRadio, { const StyledRadio = styled(RACRadio, {
base: { base: {
display: 'flex', display: 'flex',
// anchor the visually hidden input, so it scrolls with the radio
position: 'relative',
alignItems: 'center', alignItems: 'center',
gap: 0.375, gap: 0.375,
forcedColorAdjust: 'none', forcedColorAdjust: 'none',
+2
View File
@@ -9,6 +9,8 @@ import { RiCheckLine, RiCloseFill } from '@remixicon/react'
const StyledSwitch = styled(RACSwitch, { const StyledSwitch = styled(RACSwitch, {
base: { base: {
display: 'flex', display: 'flex',
// anchor the visually hidden input, so it scrolls with the switch
position: 'relative',
alignItems: 'center', alignItems: 'center',
gap: '0.571rem', gap: '0.571rem',
color: 'black', color: 'black',
+9 -2
View File
@@ -25,12 +25,15 @@ export const menuRecipe = sva({
'&[data-focused]': { '&[data-focused]': {
color: 'primary.text', color: 'primary.text',
backgroundColor: 'primaryDark.100', backgroundColor: 'primaryDark.100',
outline: 'none!',
}, },
'&[data-hovered]': { '&[data-hovered]': {
color: 'primary.text', color: 'primary.text',
backgroundColor: 'primaryDark.100', backgroundColor: 'primaryDark.100',
outline: 'none!', },
'&[data-focus-visible]': {
outline: '2px solid',
outlineColor: 'focusRing',
outlineOffset: '2px',
}, },
}, },
}, },
@@ -49,6 +52,10 @@ export const menuRecipe = sva({
dark: { dark: {
item: { item: {
color: 'white', color: 'white',
'&[data-focus-visible]': {
outlineColor: 'white!',
outlineOffset: '2px!',
},
}, },
}, },
}, },