mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-19 22:19:30 +00:00
add MeidaMuteAndSwitchButton
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
|
border-radius: 32px;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.containerOpen {
|
||||||
|
background-color: var(--cpd-color-bg-subtle-primary);
|
||||||
|
}
|
||||||
|
.menuButton {
|
||||||
|
width: 40px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
.itemIcon {
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate {
|
||||||
|
animation: spinner 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
MicOnSolidIcon,
|
||||||
|
MicOffSolidIcon,
|
||||||
|
VideoCallSolidIcon,
|
||||||
|
VideoCallOffSolidIcon,
|
||||||
|
AdvancedSettingsIcon,
|
||||||
|
VideoCallIcon,
|
||||||
|
MicOnIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: MediaMuteAndSwitchButton,
|
||||||
|
} satisfies Meta<typeof MediaMuteAndSwitchButton>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: "SomeMenu",
|
||||||
|
IconEnabled: AdvancedSettingsIcon,
|
||||||
|
IconDisabled: AdvancedSettingsIcon,
|
||||||
|
enabled: true,
|
||||||
|
options: [
|
||||||
|
{ label: "option 1", id: "1" },
|
||||||
|
{ label: "option 2", id: "2" },
|
||||||
|
],
|
||||||
|
selectedOption: "1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AudioMute: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Microphone",
|
||||||
|
IconEnabled: MicOnSolidIcon,
|
||||||
|
IconDisabled: MicOffSolidIcon,
|
||||||
|
IconOptions: MicOnIcon,
|
||||||
|
enabled: false,
|
||||||
|
options: [
|
||||||
|
{ label: "Microphone 1", id: "1" },
|
||||||
|
{ label: "Microphone 2", id: "2" },
|
||||||
|
],
|
||||||
|
selectedOption: "2",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AudioUnmute: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Microphone",
|
||||||
|
IconEnabled: MicOnSolidIcon,
|
||||||
|
IconDisabled: MicOffSolidIcon,
|
||||||
|
IconOptions: MicOnIcon,
|
||||||
|
enabled: true,
|
||||||
|
options: [
|
||||||
|
{ label: "Microphone 1", id: "1" },
|
||||||
|
{ label: "Microphone 2", id: "2" },
|
||||||
|
],
|
||||||
|
selectedOption: "2",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VideoMute: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Camera",
|
||||||
|
IconEnabled: VideoCallSolidIcon,
|
||||||
|
IconDisabled: VideoCallOffSolidIcon,
|
||||||
|
IconOptions: VideoCallIcon,
|
||||||
|
enabled: false,
|
||||||
|
options: [
|
||||||
|
{ label: "Camera 1", id: "1" },
|
||||||
|
{ label: "Camera 2", id: "2" },
|
||||||
|
],
|
||||||
|
selectedOption: "1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VideoUnmute: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Camera",
|
||||||
|
IconEnabled: VideoCallSolidIcon,
|
||||||
|
IconDisabled: VideoCallOffSolidIcon,
|
||||||
|
IconOptions: VideoCallIcon,
|
||||||
|
enabled: true,
|
||||||
|
options: [
|
||||||
|
{ label: "Camera 1", id: "1" },
|
||||||
|
{ label: "Camera 2", id: "2" },
|
||||||
|
],
|
||||||
|
toggles: [
|
||||||
|
{
|
||||||
|
label: "background blurring",
|
||||||
|
id: "background_blurring",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedOption: "2",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type ComponentType, useState, type FC } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ToggleMenuItem,
|
||||||
|
} from "@vector-im/compound-web";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import styles from "./MediaMuteAndSwitchButton.module.css";
|
||||||
|
export interface MenuOptions {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
export interface ToggleOption {
|
||||||
|
label: string;
|
||||||
|
enabled: boolean;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaMuteAndSwitchButtonProps {
|
||||||
|
/** The title used in the Switcher modal. */
|
||||||
|
title: string;
|
||||||
|
/** If the Mute button is enabled */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Callback if the mute button is clicked */
|
||||||
|
onMuteClick?: () => void;
|
||||||
|
/** The Icon used if the mute button is enabled */
|
||||||
|
IconEnabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||||
|
/** The Icon used if the mute button is disabled */
|
||||||
|
IconDisabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||||
|
/** The options available for the media device selector modal */
|
||||||
|
options?: MenuOptions[];
|
||||||
|
/** The option that will currently be rendered as the selected option */
|
||||||
|
selectedOption?: string;
|
||||||
|
/** The icon used for the different options */
|
||||||
|
IconOptions?: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||||
|
/**
|
||||||
|
* The available toggles (including there current state)
|
||||||
|
* The toggle state is not stored by this component.
|
||||||
|
* It is handled externally and needs to be set by listening to the `onSelect` callback and setting the right toggle item to `enabled`
|
||||||
|
*/
|
||||||
|
toggles?: ToggleOption[];
|
||||||
|
/**
|
||||||
|
* For any toggle and option this method will be called.
|
||||||
|
* So toggles need to be implemented by listening here and setting the right toggle item to `enabled`
|
||||||
|
*/
|
||||||
|
onSelect?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||||
|
title,
|
||||||
|
enabled,
|
||||||
|
onMuteClick,
|
||||||
|
IconEnabled,
|
||||||
|
IconDisabled,
|
||||||
|
options,
|
||||||
|
selectedOption,
|
||||||
|
IconOptions,
|
||||||
|
toggles,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
[styles.container]: true,
|
||||||
|
[styles.containerOpen]: menuOpen,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* The mute button lives inside */}
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
Icon={enabled ? IconEnabled : IconDisabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
onMuteClick?.();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
kind={enabled ? "secondary" : "primary"}
|
||||||
|
size="lg"
|
||||||
|
className={styles.button}
|
||||||
|
aria-label={t("action.edit")}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
title={title}
|
||||||
|
showTitle={true}
|
||||||
|
open={menuOpen}
|
||||||
|
onOpenChange={setMenuOpen}
|
||||||
|
side="top"
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
className={styles.menuButton}
|
||||||
|
Icon={ChevronUpIcon}
|
||||||
|
kind={"tertiary"}
|
||||||
|
size="lg"
|
||||||
|
aria-label={/*TODO*/ t("action.edit")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{options?.map((option) => (
|
||||||
|
<MenuItem
|
||||||
|
hideChevron
|
||||||
|
label={option.label}
|
||||||
|
Icon={
|
||||||
|
IconOptions && (
|
||||||
|
<IconOptions
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={styles.itemIcon}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onSelect={(e) => {
|
||||||
|
onSelect?.(option.id);
|
||||||
|
setPlannedSelection(option.id);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
key={option.id}
|
||||||
|
>
|
||||||
|
{selectedOption === option.id && (
|
||||||
|
<CheckIcon width={24} height={24} />
|
||||||
|
)}
|
||||||
|
{selectedOption !== option.id && plannedSelection === option.id && (
|
||||||
|
<SpinnerIcon width={24} height={24} className={styles.rotate} />
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
{(toggles?.length ?? 0) > 0 && <hr />}
|
||||||
|
{toggles?.map((toggle) => (
|
||||||
|
<ToggleMenuItem
|
||||||
|
label={toggle.label}
|
||||||
|
onSelect={(e) => {
|
||||||
|
onSelect?.(toggle.id);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
checked={toggle.enabled}
|
||||||
|
key={toggle.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user