Skip to content

Commit

Permalink
Replace device dropdowns with radio buttons
Browse files Browse the repository at this point in the history
This is closer to what the designs actually want device settings to look like, and it avoids the visual glitch in which the dropdown would render underneath the slider.
  • Loading branch information
robintown committed Nov 20, 2024
1 parent b3ceb53 commit 38085ba
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 56 deletions.
18 changes: 18 additions & 0 deletions src/settings/DeviceSelection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.selection {
gap: 0;
}

.title {
color: var(--cpd-color-text-secondary);
margin-block: var(--cpd-space-3x) 0;
}

.separator {
margin-block: 6px var(--cpd-space-4x);
}

.options {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
}
71 changes: 71 additions & 0 deletions src/settings/DeviceSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { ChangeEvent, FC, useCallback, useId } from "react";
import {
Heading,
InlineField,
Label,
RadioControl,
Separator,
} from "@vector-im/compound-web";

import { MediaDevice } from "../livekit/MediaDevicesContext";
import styles from "./DeviceSelection.module.css";

interface Props {
devices: MediaDevice;
caption: string;
}

export const DeviceSelection: FC<Props> = ({ devices, caption }) => {
const groupId = useId();
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
devices.select(e.target.value);
},
[devices],
);

if (devices.available.length == 0) return null;

return (
<div className={styles.selection}>
<Heading
type="body"
weight="semibold"
size="sm"
as="h4"
className={styles.title}
>
{caption}
</Heading>
<Separator className={styles.separator} />
<div className={styles.options}>
{devices.available.map(({ deviceId, label }, index) => (
<InlineField
key={deviceId}
name={groupId}
control={
<RadioControl
checked={deviceId === devices.selectedId}
onChange={onChange}
value={deviceId}
/>
}
>
<Label>
{!!label && label.trim().length > 0
? label
: `${caption} ${index + 1}`}
</Label>
</InlineField>
))}
</div>
</div>
);
};
90 changes: 34 additions & 56 deletions src/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { ChangeEvent, FC, ReactNode, useCallback } from "react";
import { ChangeEvent, FC, useCallback } from "react";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Dropdown, Separator, Text } from "@vector-im/compound-web";
import { Root as Form, Text } from "@vector-im/compound-web";

import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
Expand All @@ -19,7 +19,6 @@ import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import {
useMediaDevices,
MediaDevice,
useMediaDeviceNames,
} from "../livekit/MediaDevicesContext";
import { widget } from "../widget";
Expand All @@ -33,6 +32,7 @@ import {
import { isFirefox } from "../Platform";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider";
import { DeviceSelection } from "./DeviceSelection";

type SettingsTab =
| "audio"
Expand Down Expand Up @@ -70,40 +70,6 @@ export const SettingsModal: FC<Props> = ({
);
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);

// Generate a `SelectInput` with a list of devices for a given device kind.
const generateDeviceSelection = (
devices: MediaDevice,
caption: string,
): ReactNode => {
if (devices.available.length == 0) return null;

const values = devices.available.map(
({ deviceId, label }, index) =>
[
deviceId,
!!label && label.trim().length > 0
? label
: `${caption} ${index + 1}`,
] as [string, string],
);

return (
<Dropdown
label={caption}
defaultValue={
devices.selectedId === "" || !devices.selectedId
? "default"
: devices.selectedId
}
onValueChange={(id): void => devices.select(id)}
values={values}
// XXX This is unused because we set a defaultValue. The component
// shouldn't require this prop.
placeholder=""
/>
);
};

const optInDescription = (
<Text size="sm">
<Trans i18nKey="settings.opt_in_description">
Expand All @@ -125,33 +91,45 @@ export const SettingsModal: FC<Props> = ({
name: t("common.audio"),
content: (
<>
{generateDeviceSelection(devices.audioInput, t("common.microphone"))}
{!isFirefox() &&
generateDeviceSelection(
devices.audioOutput,
t("settings.speaker_device_selection_label"),
)}
<Separator />
<div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider
label={t("video_tile.volume")}
value={soundVolume}
onValueChange={setSoundVolume}
min={0}
max={1}
step={0.01}
<Form>
<DeviceSelection
devices={devices.audioInput}
caption={t("common.microphone")}
/>
</div>
{!isFirefox() && (
<DeviceSelection
devices={devices.audioOutput}
caption={t("settings.speaker_device_selection_label")}
/>
)}
<div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider
label={t("video_tile.volume")}
value={soundVolume}
onValueChange={setSoundVolume}
min={0}
max={1}
step={0.01}
/>
</div>
</Form>
</>
),
};

const videoTab: Tab<SettingsTab> = {
key: "video",
name: t("common.video"),
content: generateDeviceSelection(devices.videoInput, t("common.camera")),
content: (
<Form>
<DeviceSelection
devices={devices.videoInput}
caption={t("common.camera")}
/>
</Form>
),
};

const preferencesTab: Tab<SettingsTab> = {
Expand Down

0 comments on commit 38085ba

Please sign in to comment.