From 2ad612584939a0e1695c7b27917f305a38756e3e Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 30 Apr 2026 18:11:08 +0200 Subject: [PATCH] add MeidaMuteAndSwitchButton --- .../MediaMuteAndSwitchButton.module.css | 34 ++++ .../MediaMuteAndSwitchButton.stories.tsx | 107 ++++++++++++ src/components/MediaMuteAndSwitchButton.tsx | 159 ++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 src/components/MediaMuteAndSwitchButton.module.css create mode 100644 src/components/MediaMuteAndSwitchButton.stories.tsx create mode 100644 src/components/MediaMuteAndSwitchButton.tsx diff --git a/src/components/MediaMuteAndSwitchButton.module.css b/src/components/MediaMuteAndSwitchButton.module.css new file mode 100644 index 00000000..6888cbaf --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.module.css @@ -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); + } +} diff --git a/src/components/MediaMuteAndSwitchButton.stories.tsx b/src/components/MediaMuteAndSwitchButton.stories.tsx new file mode 100644 index 00000000..06506e0b --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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", + }, +}; diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx new file mode 100644 index 00000000..55e99766 --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -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>; + /** The Icon used if the mute button is disabled */ + IconDisabled: ComponentType>; + /** 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>; + /** + * 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 = ({ + title, + enabled, + onMuteClick, + IconEnabled, + IconDisabled, + options, + selectedOption, + IconOptions, + toggles, + onSelect, +}) => { + const [plannedSelection, setPlannedSelection] = useState(null); + const [menuOpen, setMenuOpen] = useState(false); + return ( +
+ {/* The mute button lives inside */} +
+ ); +};