🚸(frontend) simplify audio and video select inputs
Based on @manuhabitela's works. Align UX with common tools as Gmeet or Jitsi. Enhanced accessibility.
This commit is contained in:
committed by
aleb_the_flash
parent
96b279b350
commit
5e74fce6e2
@@ -16,8 +16,8 @@ export const Join = ({
|
|||||||
<PreJoin
|
<PreJoin
|
||||||
persistUserChoices
|
persistUserChoices
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
micLabel={t('join.micLabel')}
|
micLabel={t('join.audioinput.label')}
|
||||||
camLabel={t('join.camlabel')}
|
camLabel={t('join.videoinput.label')}
|
||||||
joinLabel={t('join.joinLabel')}
|
joinLabel={t('join.joinLabel')}
|
||||||
userLabel={t('join.userLabel')}
|
userLabel={t('join.userLabel')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
useMediaDeviceSelect,
|
||||||
|
useTrackToggle,
|
||||||
|
UseTrackToggleProps,
|
||||||
|
} from '@livekit/components-react'
|
||||||
|
import { HStack } from '@/styled-system/jsx'
|
||||||
|
import { Button, Menu, MenuList, ToggleButton } from '@/primitives'
|
||||||
|
import {
|
||||||
|
RemixiconComponentType,
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiMicLine,
|
||||||
|
RiMicOffLine,
|
||||||
|
RiVideoOffLine,
|
||||||
|
RiVideoOnLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export type ToggleSource = Exclude<
|
||||||
|
Track.Source,
|
||||||
|
Track.Source.ScreenShareAudio | Track.Source.Unknown
|
||||||
|
>
|
||||||
|
|
||||||
|
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
|
||||||
|
|
||||||
|
type SelectToggleDeviceConfig = {
|
||||||
|
kind: MediaDeviceKind
|
||||||
|
iconOn: RemixiconComponentType
|
||||||
|
iconOff: RemixiconComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectToggleDeviceConfigMap = {
|
||||||
|
[key in SelectToggleSource]: SelectToggleDeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
|
||||||
|
[Track.Source.Microphone]: {
|
||||||
|
kind: 'audioinput',
|
||||||
|
iconOn: RiMicLine,
|
||||||
|
iconOff: RiMicOffLine,
|
||||||
|
},
|
||||||
|
[Track.Source.Camera]: {
|
||||||
|
kind: 'videoinput',
|
||||||
|
iconOn: RiVideoOnLine,
|
||||||
|
iconOff: RiVideoOffLine,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||||
|
UseTrackToggleProps<T> & {
|
||||||
|
onActiveDeviceChange: (deviceId: string) => void
|
||||||
|
source: SelectToggleSource
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectToggleDevice = <T extends ToggleSource>({
|
||||||
|
onActiveDeviceChange,
|
||||||
|
...props
|
||||||
|
}: SelectToggleDeviceProps<T>) => {
|
||||||
|
const config = selectToggleDeviceConfig[props.source]
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Invalid source')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
const { buttonProps, enabled } = useTrackToggle(props)
|
||||||
|
|
||||||
|
const { kind, iconOn, iconOff } = config
|
||||||
|
|
||||||
|
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||||
|
useMediaDeviceSelect({ kind })
|
||||||
|
|
||||||
|
const toggleLabel = t(enabled ? 'disable' : 'enable', {
|
||||||
|
keyPrefix: `join.${kind}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectLabel = t('choose', { keyPrefix: `join.${kind}` })
|
||||||
|
const Icon = enabled ? iconOn : iconOff
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack gap={0}>
|
||||||
|
<ToggleButton
|
||||||
|
isSelected={enabled}
|
||||||
|
variant={enabled ? undefined : 'danger'}
|
||||||
|
toggledStyles={false}
|
||||||
|
onPress={(e) =>
|
||||||
|
buttonProps.onClick?.(
|
||||||
|
e as unknown as React.MouseEvent<HTMLButtonElement>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
aria-label={toggleLabel}
|
||||||
|
tooltip={toggleLabel}
|
||||||
|
groupPosition="left"
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</ToggleButton>
|
||||||
|
<Menu>
|
||||||
|
<Button
|
||||||
|
tooltip={selectLabel}
|
||||||
|
aria-label={selectLabel}
|
||||||
|
groupPosition="right"
|
||||||
|
square
|
||||||
|
>
|
||||||
|
<RiArrowDownSLine />
|
||||||
|
</Button>
|
||||||
|
<MenuList
|
||||||
|
items={devices.map((d) => ({
|
||||||
|
value: d.deviceId,
|
||||||
|
label: d.label,
|
||||||
|
}))}
|
||||||
|
selectedItem={activeDeviceId}
|
||||||
|
onAction={(value) => {
|
||||||
|
setActiveMediaDevice(value as string)
|
||||||
|
onActiveDeviceChange(value as string)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { supportsScreenSharing } from '@livekit/components-core'
|
|||||||
import {
|
import {
|
||||||
DisconnectButton,
|
DisconnectButton,
|
||||||
LeaveIcon,
|
LeaveIcon,
|
||||||
MediaDeviceMenu,
|
|
||||||
TrackToggle,
|
TrackToggle,
|
||||||
useMaybeLayoutContext,
|
useMaybeLayoutContext,
|
||||||
usePersistentUserChoices,
|
usePersistentUserChoices,
|
||||||
@@ -20,6 +19,7 @@ import { OptionsButton } from '../components/controls/Options/OptionsButton'
|
|||||||
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
|
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
|
||||||
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
|
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
|
||||||
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
|
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
|
||||||
|
import { SelectToggleDevice } from '@/features/rooms/livekit/components/controls/SelectToggleDevice'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type ControlBarControls = {
|
export type ControlBarControls = {
|
||||||
@@ -126,46 +126,26 @@ export function ControlBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...htmlProps}>
|
<div {...htmlProps}>
|
||||||
<div className="lk-button-group">
|
<SelectToggleDevice
|
||||||
<TrackToggle
|
source={Track.Source.Microphone}
|
||||||
source={Track.Source.Microphone}
|
onChange={microphoneOnChange}
|
||||||
showIcon={showIcon}
|
onDeviceError={(error) =>
|
||||||
onChange={microphoneOnChange}
|
onDeviceError?.({ source: Track.Source.Microphone, error })
|
||||||
onDeviceError={(error) =>
|
}
|
||||||
onDeviceError?.({ source: Track.Source.Microphone, error })
|
onActiveDeviceChange={(deviceId) =>
|
||||||
}
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
>
|
}
|
||||||
{showText && t('controls.microphone')}
|
/>
|
||||||
</TrackToggle>
|
<SelectToggleDevice
|
||||||
<div className="lk-button-group-menu">
|
source={Track.Source.Camera}
|
||||||
<MediaDeviceMenu
|
onChange={cameraOnChange}
|
||||||
kind="audioinput"
|
onDeviceError={(error) =>
|
||||||
onActiveDeviceChange={(_kind, deviceId) =>
|
onDeviceError?.({ source: Track.Source.Camera, error })
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
}
|
||||||
}
|
onActiveDeviceChange={(deviceId) =>
|
||||||
/>
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
<div className="lk-button-group">
|
|
||||||
<TrackToggle
|
|
||||||
source={Track.Source.Camera}
|
|
||||||
showIcon={showIcon}
|
|
||||||
onChange={cameraOnChange}
|
|
||||||
onDeviceError={(error) =>
|
|
||||||
onDeviceError?.({ source: Track.Source.Camera, error })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{showText && t('controls.camera')}
|
|
||||||
</TrackToggle>
|
|
||||||
<div className="lk-button-group-menu">
|
|
||||||
<MediaDeviceMenu
|
|
||||||
kind="videoinput"
|
|
||||||
onActiveDeviceChange={(_kind, deviceId) =>
|
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{browserSupportsScreenSharing && (
|
{browserSupportsScreenSharing && (
|
||||||
<TrackToggle
|
<TrackToggle
|
||||||
source={Track.Source.ScreenShare}
|
source={Track.Source.ScreenShare}
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
"heading": ""
|
"heading": ""
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "",
|
"videoinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "",
|
||||||
|
"disable": "",
|
||||||
|
"enable": "",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
"heading": "",
|
"heading": "",
|
||||||
"joinLabel": "",
|
"joinLabel": "",
|
||||||
"micLabel": "",
|
"joinMeeting": "",
|
||||||
"userLabel": ""
|
"toggleOff": "",
|
||||||
|
"toggleOn": "",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "",
|
||||||
|
"usernameLabel": ""
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "",
|
"leaveRoomPrompt": "",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
"heading": "Help us improve Meet"
|
"heading": "Help us improve Meet"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Camera",
|
"videoinput": {
|
||||||
"heading": "Join the meeting",
|
"choose": "Select camera",
|
||||||
|
"disable": "Disable camera",
|
||||||
|
"enable": "Enable camera",
|
||||||
|
"label": "Camera",
|
||||||
|
"placeholder": "Enable camera to see the preview"
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "Select microphone",
|
||||||
|
"disable": "Disable microphone",
|
||||||
|
"enable": "Enable microphone",
|
||||||
|
"label": "Microphone"
|
||||||
|
},
|
||||||
|
"heading": "Verify your settings",
|
||||||
"joinLabel": "Join",
|
"joinLabel": "Join",
|
||||||
"micLabel": "Microphone",
|
"joinMeeting": "Join meeting",
|
||||||
"userLabel": "Your name"
|
"toggleOff": "Click to turn off",
|
||||||
|
"toggleOn": "Click to turn on",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Shown to other participants",
|
||||||
|
"usernameLabel": "Your name"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
"heading": "Aidez-nous à améliorer Meet"
|
"heading": "Aidez-nous à améliorer Meet"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"camlabel": "Webcam",
|
"videoinput": {
|
||||||
"heading": "Rejoindre la réunion",
|
"choose": "Choisir la webcam",
|
||||||
|
"disable": "Désactiver la webcam",
|
||||||
|
"enable": "Activer la webcam",
|
||||||
|
"label": "Webcam",
|
||||||
|
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
|
||||||
|
},
|
||||||
|
"audioinput": {
|
||||||
|
"choose": "Choisir le micro",
|
||||||
|
"disable": "Désactiver le micro",
|
||||||
|
"enable": "Activer le micro",
|
||||||
|
"label": "Microphone"
|
||||||
|
},
|
||||||
|
"heading": "Vérifiez vos paramètres",
|
||||||
"joinLabel": "Rejoindre",
|
"joinLabel": "Rejoindre",
|
||||||
"micLabel": "Micro",
|
"joinMeeting": "Rejoindre la réjoindre",
|
||||||
"userLabel": "Votre nom"
|
"toggleOff": "Cliquez pour désactiver",
|
||||||
|
"toggleOn": "Cliquez pour activer",
|
||||||
|
"userLabel": "",
|
||||||
|
"usernameHint": "Affiché aux autres participants",
|
||||||
|
"usernameLabel": "Votre nom"
|
||||||
},
|
},
|
||||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||||
"shareDialog": {
|
"shareDialog": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
|
|||||||
const label = typeof item === 'string' ? item : item.label
|
const label = typeof item === 'string' ? item : item.label
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuItemRecipe()}
|
className={menuItemRecipe({ extraPadding: true })}
|
||||||
key={value}
|
key={value}
|
||||||
id={value as string}
|
id={value as string}
|
||||||
onAction={() => {
|
onAction={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user