Finish settings modal

This commit is contained in:
Robert Long 2021-12-06 17:34:10 -08:00
parent 4e2d1c5dcd
commit 94f42019df
28 changed files with 975 additions and 356 deletions

View file

@ -11,9 +11,12 @@
"@react-aria/focus": "^3.5.0",
"@react-aria/menu": "^3.3.0",
"@react-aria/overlays": "^3.7.3",
"@react-aria/select": "^3.6.0",
"@react-aria/tabs": "^3.1.0",
"@react-aria/utils": "^3.10.0",
"@react-stately/collections": "^3.3.4",
"@react-stately/overlays": "^3.1.3",
"@react-stately/select": "^3.1.3",
"@react-stately/tree": "^3.2.0",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",

View file

@ -30,6 +30,7 @@ import { RegisterPage } from "./RegisterPage";
import { LoginPage } from "./LoginPage";
import { Center } from "./Layout";
import { GuestAuthPage } from "./GuestAuthPage";
import { OverlayProvider } from "@react-aria/overlays";
const SentryRoute = Sentry.withSentryRouting(Route);
@ -49,7 +50,7 @@ export default function App() {
return (
<Router>
<>
<OverlayProvider>
{loading ? (
<Center>
<p>Loading...</p>
@ -74,7 +75,7 @@ export default function App() {
</SentryRoute>
</Switch>
)}
</>
</OverlayProvider>
</Router>
);
}

View file

@ -1,36 +1,36 @@
import React, { useCallback } from "react";
import { ButtonTooltip, HeaderButton } from "./RoomButton";
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
import styles from "./GridLayoutMenu.module.css";
import { Menu } from "./Menu";
import { Item } from "@react-stately/collections";
export function GridLayoutMenu({ layout, setLayout }) {
const onAction = useCallback((value) => setLayout(value));
return (
<PopoverMenu onAction={onAction} placement="bottom right">
<PopoverMenuTrigger placement="bottom right">
<HeaderButton>
<ButtonTooltip>Layout Type</ButtonTooltip>
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</HeaderButton>
{(props) => (
<Popover {...props} label="Grid layout menu">
<PopoverMenuItem key="freedom" textValue="Freedom">
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
</PopoverMenuItem>
<PopoverMenuItem key="spotlight" textValue="Spotlight">
</Item>
<Item key="spotlight" textValue="Spotlight">
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (
<CheckIcon className={styles.checkIcon} />
)}
</PopoverMenuItem>
</Popover>
</Item>
</Menu>
)}
</PopoverMenu>
</PopoverMenuTrigger>
);
}

View file

@ -1,25 +1,14 @@
import React from "react";
import { Overlay } from "./Overlay";
import { Modal, ModalContent } from "./Modal";
import { CopyButton } from "./CopyButton";
import { HeaderButton, ButtonTooltip } from "./RoomButton";
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
export function InviteModalButton({ roomUrl }) {
export function InviteModal({ roomUrl, ...rest }) {
return (
<Overlay>
<HeaderButton>
<ButtonTooltip>Add User</ButtonTooltip>
<AddUserIcon width={20} height={20} />
</HeaderButton>
{(modalProps) => (
<Modal title="Invite People" isDismissable {...modalProps}>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton value={roomUrl} />
</ModalContent>
</Modal>
)}
</Overlay>
<Modal title="Invite People" isDismissable {...rest}>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton value={roomUrl} />
</ModalContent>
</Modal>
);
}

50
src/ListBox.jsx Normal file
View file

@ -0,0 +1,50 @@
import React, { useRef } from "react";
import { useListBox, useOption } from "@react-aria/listbox";
import styles from "./ListBox.module.css";
import classNames from "classnames";
export function ListBox(props) {
const ref = useRef();
let { listBoxRef = ref, state } = props;
const { listBoxProps } = useListBox(props, state, listBoxRef);
return (
<ul
{...listBoxProps}
ref={listBoxRef}
className={classNames(styles.listBox, props.className)}
>
{[...state.collection].map((item) => (
<Option
key={item.key}
item={item}
state={state}
className={props.optionClassName}
/>
))}
</ul>
);
}
function Option({ item, state, className }) {
const ref = useRef();
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key },
state,
ref
);
return (
<li
{...optionProps}
ref={ref}
className={classNames(styles.option, className, {
[styles.selected]: isSelected,
[styles.focused]: isFocused,
[styles.disables]: isDisabled,
})}
>
{item.rendered}
</li>
);
}

34
src/ListBox.module.css Normal file
View file

@ -0,0 +1,34 @@
.listBox {
margin: 0;
padding: 0;
max-height: 150px;
overflow-y: auto;
list-style: none;
background-color: transparent;
}
.option {
display: flex;
align-items: center;
justify-content: space-between;
background-color: transparent;
color: var(--textColor1);
padding: 8px 16px;
outline: none;
cursor: pointer;
font-size: 15px;
min-height: 32px;
}
.option.selected {
color: #0dbd8b;
}
.option.focused {
background-color: var(--bgColor3);
}
.option.disabled {
color: var(--textColor2);
background-color: var(--bgColor3);
}

60
src/Menu.jsx Normal file
View file

@ -0,0 +1,60 @@
import React, { useRef, useState } from "react";
import styles from "./Menu.module.css";
import { useMenu, useMenuItem } from "@react-aria/menu";
import { useTreeState } from "@react-stately/tree";
import { mergeProps } from "@react-aria/utils";
import { useFocus } from "@react-aria/interactions";
import classNames from "classnames";
export function Menu({ className, onAction, ...rest }) {
const state = useTreeState({ ...rest, selectionMode: "none" });
const menuRef = useRef();
const { menuProps } = useMenu(rest, state, menuRef);
return (
<ul
{...mergeProps(menuProps, rest)}
ref={menuRef}
className={classNames(styles.menu, className)}
>
{[...state.collection].map((item) => (
<MenuItem
key={item.key}
item={item}
state={state}
onAction={onAction}
onClose={rest.onClose}
/>
))}
</ul>
);
}
function MenuItem({ item, state, onAction, onClose }) {
const ref = useRef();
const { menuItemProps } = useMenuItem(
{
key: item.key,
isDisabled: item.isDisabled,
onAction,
onClose,
},
state,
ref
);
const [isFocused, setFocused] = useState(false);
const { focusProps } = useFocus({ onFocusChange: setFocused });
return (
<li
{...mergeProps(menuItemProps, focusProps)}
ref={ref}
className={classNames(styles.menuItem, {
[styles.focused]: isFocused,
})}
>
{item.rendered}
</li>
);
}

40
src/Menu.module.css Normal file
View file

@ -0,0 +1,40 @@
.menu {
width: 100%;
padding: 0;
margin: 0;
list-style: none;
}
.menuItem {
cursor: pointer;
height: 48px;
display: flex;
align-items: center;
padding: 0 12px;
color: var(--textColor1);
font-size: 14px;
}
.menuItem > * {
margin-right: 10px;
}
.menuItem > :last-child {
margin-right: 0;
}
.menuItem.focused,
.menuItem:hover {
background-color: var(--bgColor3);
outline: none;
}
.menuItem.focused:first-child,
.menuItem:hover:first-child {
border-radius: 8px 8px 0 0;
}
.menuItem.focused:last-child,
.menuItem:hover:last-child {
border-radius: 0 0 8px 8px;
}

View file

@ -1,10 +1,11 @@
import React, { useRef } from "react";
import React, { useRef, useMemo } from "react";
import {
useOverlay,
usePreventScroll,
useModal,
OverlayContainer,
} from "@react-aria/overlays";
import { useOverlayTriggerState } from "@react-stately/overlays";
import { useDialog } from "@react-aria/dialog";
import { FocusScope } from "@react-aria/focus";
import { useButton } from "@react-aria/button";
@ -13,7 +14,7 @@ import styles from "./Modal.module.css";
import classNames from "classnames";
export function Modal(props) {
const { title, children } = props;
const { title, children, className } = props;
const modalRef = useRef();
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
usePreventScroll();
@ -33,7 +34,7 @@ export function Modal(props) {
{...dialogProps}
{...modalProps}
ref={modalRef}
className={styles.modal}
className={classNames(styles.modal, className)}
>
<div className={styles.modalHeader}>
<h3 {...titleProps}>{title}</h3>
@ -60,3 +61,68 @@ export function ModalContent({ children, className, ...rest }) {
</div>
);
}
export function useModalTriggerState() {
const modalState = useOverlayTriggerState({});
const modalProps = useMemo(
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
[modalState]
);
return { modalState, modalProps };
}
export function useToggleModalButton(modalState, ref) {
return useButton(
{
onPress: () => modalState.toggle(),
},
ref
);
}
export function useOpenModalButton(modalState, ref) {
return useButton(
{
onPress: () => modalState.open(),
},
ref
);
}
export function useCloseModalButton(modalState, ref) {
return useButton(
{
onPress: () => modalState.close(),
},
ref
);
}
export function ModalTrigger({ children }) {
const { modalState, modalProps } = useModalState();
const buttonRef = useRef();
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
if (
!Array.isArray(children) ||
children.length > 2 ||
typeof children[1] !== "function"
) {
throw new Error(
"ModalTrigger must have two props. The first being a button and the second being a render prop."
);
}
const [modalTrigger, modal] = children;
return (
<>
<modalTrigger.type
{...modalTrigger.props}
{...buttonProps}
ref={buttonRef}
/>
{modalState.isOpen && modal(modalProps)}
</>
);
}

View file

@ -1,31 +1,70 @@
import React, { useCallback } from "react";
import { ButtonTooltip, RoomButton } from "./RoomButton";
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
import { Menu } from "./Menu";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { Item } from "@react-stately/collections";
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "./icons/Overflow.svg";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./SettingsModal";
import { InviteModal } from "./InviteModal";
export function OverflowMenu({ roomUrl }) {
const onAction = useCallback((e) => console.log(e));
export function OverflowMenu({
roomUrl,
setShowInspector,
showInspector,
client,
}) {
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444
const onAction = useCallback((key) => {
switch (key) {
case "invite":
inviteModalState.open();
break;
case "settings":
settingsModalState.open();
break;
}
});
return (
<PopoverMenu onAction={onAction}>
<RoomButton>
<ButtonTooltip>More</ButtonTooltip>
<OverflowIcon />
</RoomButton>
{(props) => (
<Popover {...props} label="More menu">
<PopoverMenuItem key="invite" textValue="Invite people">
<AddUserIcon />
<span>Invite people</span>
</PopoverMenuItem>
<PopoverMenuItem key="settings" textValue="Settings">
<SettingsIcon />
<span>Settings</span>
</PopoverMenuItem>
</Popover>
<>
<PopoverMenuTrigger>
<RoomButton>
<ButtonTooltip>More</ButtonTooltip>
<OverflowIcon />
</RoomButton>
{(props) => (
<Menu {...props} label="More menu" onAction={onAction}>
<Item key="invite" textValue="Invite people">
<AddUserIcon />
<span>Invite people</span>
</Item>
<Item key="settings" textValue="Settings">
<SettingsIcon />
<span>Settings</span>
</Item>
</Menu>
)}
</PopoverMenuTrigger>
{settingsModalState.isOpen && (
<SettingsModal
{...settingsModalProps}
setShowInspector={setShowInspector}
showInspector={showInspector}
client={client}
/>
)}
</PopoverMenu>
{inviteModalState.isOpen && (
<InviteModal roomUrl={roomUrl} {...inviteModalProps} />
)}
</>
);
}

View file

@ -1,60 +0,0 @@
import React, { useRef } from "react";
import { useOverlayTriggerState } from "@react-stately/overlays";
import { useButton } from "@react-aria/button";
export function useToggleOverlayButton(overlayState, ref) {
return useButton(
{
onPress: () => overlayState.toggle(),
},
ref
);
}
export function useOpenOverlayButton(overlayState, ref) {
return useButton(
{
onPress: () => overlayState.open(),
},
ref
);
}
export function useCloseOverlayButton(overlayState, ref) {
return useButton(
{
onPress: () => overlayState.close(),
},
ref
);
}
export function Overlay({ children }) {
const overlayState = useOverlayTriggerState({});
const buttonRef = useRef();
const { buttonProps } = useToggleOverlayButton(overlayState, buttonRef);
if (
!Array.isArray(children) ||
children.length > 2 ||
typeof children[1] !== "function"
) {
throw new Error(
"Overlay trigger must have two props. The first being a button and the second being a render prop."
);
}
const [overlayTrigger, overlay] = children;
return (
<>
<overlayTrigger.type
{...overlayTrigger.props}
{...buttonProps}
ref={buttonRef}
/>
{overlayState.isOpen &&
overlay({ isOpen: overlayState.isOpen, onClose: overlayState.close })}
</>
);
}

39
src/Popover.jsx Normal file
View file

@ -0,0 +1,39 @@
import React, { forwardRef } from "react";
import {
DismissButton,
useOverlay,
OverlayContainer,
} from "@react-aria/overlays";
import { FocusScope } from "@react-aria/focus";
import classNames from "classnames";
import styles from "./Popover.module.css";
export const Popover = forwardRef(
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
const { overlayProps } = useOverlay(
{
isOpen,
onClose,
shouldCloseOnBlur: true,
isDismissable: true,
},
ref
);
return (
<OverlayContainer>
<FocusScope restoreFocus>
<div
{...overlayProps}
{...rest}
className={classNames(styles.popover, className)}
ref={ref}
>
{children}
<DismissButton onDismiss={onClose} />
</div>
</FocusScope>
</OverlayContainer>
);
}
);

8
src/Popover.module.css Normal file
View file

@ -0,0 +1,8 @@
.popover {
display: flex;
flex-direction: column;
width: 194px;
background: var(--bgColor2);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}

View file

@ -1,22 +1,17 @@
import React, { useRef, useState, forwardRef } from "react";
import React, { useRef } from "react";
import styles from "./PopoverMenu.module.css";
import { useMenuTriggerState } from "@react-stately/menu";
import { useButton } from "@react-aria/button";
import { useMenu, useMenuItem, useMenuTrigger } from "@react-aria/menu";
import { useTreeState } from "@react-stately/tree";
import { Item } from "@react-stately/collections";
import { mergeProps } from "@react-aria/utils";
import { FocusScope } from "@react-aria/focus";
import { useFocus } from "@react-aria/interactions";
import {
useOverlay,
DismissButton,
useOverlayPosition,
OverlayContainer,
} from "@react-aria/overlays";
import { useMenuTrigger } from "@react-aria/menu";
import { useOverlayPosition } from "@react-aria/overlays";
import classNames from "classnames";
import { Popover } from "./Popover";
export function PopoverMenu({ children, placement, ...rest }) {
export function PopoverMenuTrigger({
children,
placement,
className,
...rest
}) {
const popoverMenuState = useMenuTriggerState(rest);
const buttonRef = useRef();
const { menuTriggerProps, menuProps } = useMenuTrigger(
@ -27,7 +22,7 @@ export function PopoverMenu({ children, placement, ...rest }) {
const popoverRef = useRef();
const { overlayProps: positionProps } = useOverlayPosition({
const { overlayProps } = useOverlayPosition({
targetRef: buttonRef,
overlayRef: popoverRef,
placement: placement || "top",
@ -45,102 +40,30 @@ export function PopoverMenu({ children, placement, ...rest }) {
);
}
const [popoverTrigger, popover] = children;
const [popoverTrigger, popoverMenu] = children;
return (
<div style={{ position: "relative", display: "inline-block" }}>
<div className={classNames(styles.popoverMenuTrigger, className)}>
<popoverTrigger.type
{...popoverTrigger.props}
{...menuTriggerProps}
on={popoverMenuState.isOpen}
ref={buttonRef}
/>
{popoverMenuState.isOpen &&
popover({
isOpen: popoverMenuState.isOpen,
onClose: popoverMenuState.close,
autoFocus: popoverMenuState.focusStrategy,
domProps: menuProps,
ref: popoverRef,
positionProps,
...rest,
})}
{popoverMenuState.isOpen && (
<Popover
{...overlayProps}
isOpen={popoverMenuState.isOpen}
onClose={popoverMenuState.close}
ref={popoverRef}
>
{popoverMenu({
...menuProps,
autoFocus: popoverMenuState.focusStrategy,
onClose: popoverMenuState.close,
})}
</Popover>
)}
</div>
);
}
export const Popover = forwardRef((props, ref) => {
const state = useTreeState({ ...props, selectionMode: "none" });
const menuRef = useRef();
const { menuProps } = useMenu(props, state, menuRef);
const { overlayProps } = useOverlay(
{
onClose: props.onClose,
shouldCloseOnBlur: true,
isOpen: true,
isDismissable: true,
},
ref
);
return (
<OverlayContainer>
<FocusScope restoreFocus>
<div
className={styles.popover}
{...mergeProps(overlayProps, props.positionProps)}
ref={ref}
>
<DismissButton onDismiss={props.onClose} />
<ul
{...mergeProps(menuProps, props.domProps)}
ref={menuRef}
className={styles.popoverMenu}
>
{[...state.collection].map((item) => (
<PopoverMenuItemContainer
key={item.key}
item={item}
state={state}
onAction={props.onAction}
onClose={props.onClose}
/>
))}
</ul>
<DismissButton onDismiss={props.onClose} />
</div>
</FocusScope>
</OverlayContainer>
);
});
function PopoverMenuItemContainer({ item, state, onAction, onClose }) {
const ref = useRef();
const { menuItemProps } = useMenuItem(
{
key: item.key,
isDisabled: item.isDisabled,
onAction,
onClose,
},
state,
ref
);
const [isFocused, setFocused] = useState(false);
const { focusProps } = useFocus({ onFocusChange: setFocused });
return (
<li
{...mergeProps(menuItemProps, focusProps)}
ref={ref}
className={classNames(styles.popoverMenuItem, {
[styles.focused]: isFocused,
})}
>
{item.rendered}
</li>
);
}
export const PopoverMenuItem = Item;

View file

@ -1,49 +1,4 @@
.popover {
display: flex;
flex-direction: column;
width: 194px;
background: var(--bgColor2);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}
.popoverMenu {
width: 100%;
padding: 0;
margin: 0;
list-style: none;
}
.popoverMenuItem {
cursor: pointer;
height: 48px;
display: flex;
align-items: center;
padding: 0 12px;
color: var(--textColor1);
font-size: 14px;
}
.popoverMenuItem > * {
margin-right: 10px;
}
.popoverMenuItem > :last-child {
margin-right: 0;
}
.popoverMenuItem.focused,
.popoverMenuItem:hover {
background-color: var(--bgColor3);
outline: none;
}
.popoverMenuItem.focused:first-child,
.popoverMenuItem:hover:first-child {
border-radius: 8px 8px 0 0;
}
.popoverMenuItem.focused:last-child,
.popoverMenuItem:hover:last-child {
border-radius: 0 0 8px 8px;
.popoverMenuTrigger {
position: relative;
display: inline-block;
}

View file

@ -21,7 +21,6 @@ import {
HangupButton,
MicButton,
VideoButton,
LayoutToggleButton,
ScreenshareButton,
DropdownButton,
} from "./RoomButton";
@ -47,10 +46,10 @@ import { fetchGroupCall } from "./ConferenceCallManagerHooks";
import { ErrorModal } from "./ErrorModal";
import { GroupCallInspector } from "./GroupCallInspector";
import * as Sentry from "@sentry/react";
import { InviteModalButton } from "./InviteModal";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { UserMenu } from "./UserMenu";
import { useMediaHandler } from "./useMediaHandler";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -351,77 +350,6 @@ function RoomSetupView({
);
}
function useMediaHandler(client) {
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
useState(() => {
const mediaHandler = client.getMediaHandler();
return {
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs: [],
videoInputs: [],
};
});
useEffect(() => {
const mediaHandler = client.getMediaHandler();
function updateDevices() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);
setState((prevState) => ({
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs,
videoInputs,
}));
});
}
updateDevices();
mediaHandler.on("local_streams_changed", updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => {
mediaHandler.removeListener("local_streams_changed", updateDevices);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
};
}, []);
const setAudioInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client.getMediaHandler().setAudioInput(deviceId);
},
[client]
);
const setVideoInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
client.getMediaHandler().setVideoInput(deviceId);
},
[client]
);
return {
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
};
}
function InRoomView({
onLogout,
client,
@ -443,15 +371,6 @@ function InRoomView({
const [layout, setLayout] = useVideoGridLayout();
const {
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
} = useMediaHandler(client);
const items = useMemo(() => {
const participants = [];
@ -539,7 +458,12 @@ function InRoomView({
onPress={toggleScreensharing}
/>
)}
<OverflowMenu roomUrl={window.location.href} />
<OverflowMenu
roomUrl={window.location.href}
setShowInspector={setShowInspector}
showInspector={showInspector}
client={client}
/>
<HangupButton onPress={onLeave} />
</div>
<GroupCallInspector

71
src/SelectInput.jsx Normal file
View file

@ -0,0 +1,71 @@
import React, { useRef } from "react";
import { HiddenSelect, useSelect } from "@react-aria/select";
import { useButton } from "@react-aria/button";
import { useSelectState } from "@react-stately/select";
import { Popover } from "./Popover";
import { ListBox } from "./ListBox";
import { useOverlayPosition } from "@react-aria/overlays";
import styles from "./SelectInput.module.css";
import classNames from "classnames";
import { ReactComponent as ArrowDownIcon } from "./icons/ArrowDown.svg";
export function SelectInput(props) {
const state = useSelectState(props);
const ref = useRef();
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
props,
state,
ref
);
const { buttonProps } = useButton(triggerProps, ref);
const popoverRef = useRef();
const { overlayProps } = useOverlayPosition({
targetRef: ref,
overlayRef: popoverRef,
placement: "bottom left",
offset: 5,
isOpen: state.isOpen,
});
return (
<div className={classNames(styles.selectInput, props.className)}>
<h4 {...labelProps} className={styles.label}>
{props.label}
</h4>
<HiddenSelect
state={state}
triggerRef={ref}
label={props.label}
name={props.name}
/>
<button {...buttonProps} ref={ref} className={styles.selectTrigger}>
<span {...valueProps} className={styles.selectedItem}>
{state.selectedItem
? state.selectedItem.rendered
: "Select an option"}
</span>
<ArrowDownIcon />
</button>
{state.isOpen && (
<Popover
ref={popoverRef}
isOpen={state.isOpen}
onClose={state.close}
className={styles.popover}
{...overlayProps}
>
<ListBox
{...menuProps}
className
state={state}
optionClassName={styles.option}
/>
</Popover>
)}
</div>
);
}

View file

@ -0,0 +1,45 @@
.selectInput {
position: relative;
display: inline-block;
margin-bottom: 28px;
}
.label {
font-weight: 600;
font-size: 18px;
margin-top: 0;
margin-bottom: 12px;
}
.selectTrigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background-color: var(--bgColor1);
border-radius: 8px;
border: 1px solid var(--inputBorderColor);
font-size: 15px;
color: var(--textColor1);
height: 40px;
width: 320px;
}
.selectedItem {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 20px;
}
.popover {
width: 320px;
}
.option:first-child {
border-radius: 8px 8px 0 0;
}
.option:last-child {
border-radius: 0 0 8px 8px;
}

94
src/SettingsModal.jsx Normal file
View file

@ -0,0 +1,94 @@
import React from "react";
import { Modal } from "./Modal";
import styles from "./SettingsModal.module.css";
import { TabContainer, TabItem } from "./Tabs";
import { ReactComponent as AudioIcon } from "./icons/Audio.svg";
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
import { ReactComponent as DeveloperIcon } from "./icons/Developer.svg";
import { SelectInput } from "./SelectInput";
import { Item } from "@react-stately/collections";
import { useMediaHandler } from "./useMediaHandler";
import { FieldRow, InputField } from "./Input";
export function SettingsModal({
client,
setShowInspector,
showInspector,
...rest
}) {
const {
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
} = useMediaHandler(client);
return (
<Modal
title="Settings"
isDismissable
className={styles.settingsModal}
{...rest}
>
<TabContainer className={styles.tabContainer}>
<TabItem
title={
<>
<AudioIcon width={16} height={16} />
<span>Audio</span>
</>
}
>
<SelectInput
label="Microphone"
selectedKey={audioInput}
onSelectionChange={setAudioInput}
>
{audioInputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
</TabItem>
<TabItem
title={
<>
<VideoIcon width={16} height={16} />
<span>Video</span>
</>
}
>
<SelectInput
label="Webcam"
selectedKey={videoInput}
onSelectionChange={setVideoInput}
>
{videoInputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
</TabItem>
<TabItem
title={
<>
<DeveloperIcon width={16} height={16} />
<span>Developer</span>
</>
}
>
<FieldRow>
<InputField
id="showInspector"
name="inspector"
label="Show Call Inspector"
type="checkbox"
checked={showInspector}
onChange={(e) => setShowInspector(e.target.checked)}
/>
</FieldRow>
</TabItem>
</TabContainer>
</Modal>
);
}

View file

@ -0,0 +1,8 @@
.settingsModal {
width: 774px;
height: 480px;
}
.tabContainer {
margin: 27px 16px;
}

53
src/Tabs.jsx Normal file
View file

@ -0,0 +1,53 @@
import React, { useRef } from "react";
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
import { Item } from "@react-stately/collections";
import { useTabListState } from "@react-stately/tabs";
import styles from "./Tabs.module.css";
import classNames from "classnames";
export function TabContainer(props) {
const state = useTabListState(props);
const ref = useRef();
const { tabListProps } = useTabList(props, state, ref);
return (
<div className={classNames(styles.tabContainer, props.className)}>
<ul {...tabListProps} ref={ref} className={styles.tabList}>
{[...state.collection].map((item) => (
<Tab key={item.key} item={item} state={state} />
))}
</ul>
<TabPanel key={state.selectedItem?.key} state={state} />
</div>
);
}
function Tab({ item, state }) {
const { key, rendered } = item;
const ref = useRef();
const { tabProps } = useTab({ key }, state, ref);
return (
<li
{...tabProps}
ref={ref}
className={classNames(styles.tab, {
[styles.selected]: state.selectedKey === key,
[styles.disabled]: state.disabledKeys.has(key),
})}
>
{rendered}
</li>
);
}
function TabPanel({ state, ...props }) {
const ref = useRef();
const { tabPanelProps } = useTabPanel(props, state, ref);
return (
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}>
{state.selectedItem?.props.children}
</div>
);
}
export const TabItem = Item;

59
src/Tabs.module.css Normal file
View file

@ -0,0 +1,59 @@
.tabContainer {
width: 100%;
display: flex;
}
.tabList {
display: flex;
flex-direction: column;
width: 190px;
list-style: none;
padding: 0;
margin: 0;
}
.tab {
height: 32px;
border-radius: 8px;
background-color: transparent;
display: flex;
align-items: center;
padding: 0 16px;
border: none;
cursor: pointer;
}
.tab > * {
color: var(--textColor2);
margin-right: 16px;
}
.tab svg * {
fill: var(--textColor2);
}
.tab > :last-child {
margin-right: 0;
}
.tab.selected {
background-color: #0dbd8b;
}
.tab.selected * {
color: #ffffff;
}
.tab.selected svg * {
fill: #ffffff;
}
.tab.disabled {
}
.tabPanel {
display: flex;
flex-direction: column;
flex: 1;
padding: 0 40px;
}

View file

@ -1,10 +1,12 @@
import React, { useCallback, useMemo } from "react";
import { ButtonTooltip, HeaderButton } from "./RoomButton";
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
import { PopoverMenuTrigger } from "./PopoverMenu";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
import styles from "./UserMenu.module.css";
import { Item } from "@react-stately/collections";
import { Menu } from "./Menu";
export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
const onAction = useCallback((value) => {
@ -46,21 +48,21 @@ export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
}, [signedIn, userName]);
return (
<PopoverMenu onAction={onAction} placement="bottom right">
<PopoverMenuTrigger onAction={onAction} placement="bottom right">
<HeaderButton className={styles.userButton}>
<ButtonTooltip>Profile</ButtonTooltip>
<UserIcon />
</HeaderButton>
{(props) => (
<Popover {...props} label="User menu">
<Menu {...props} label="User menu">
{items.map(({ key, icon: Icon, label }) => (
<PopoverMenuItem key={key} textValue={label}>
<Item key={key} textValue={label}>
<Icon />
<span>{label}</span>
</PopoverMenuItem>
</Item>
))}
</Popover>
</Menu>
)}
</PopoverMenu>
</PopoverMenuTrigger>
);
}

10
src/icons/ArrowDown.svg Normal file
View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_965_9448)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.70711 5.36321C2.09763 4.97268 2.73182 4.97166 3.1236 5.36091L8.08934 10.2946L13.0391 5.34488C13.4296 4.95435 14.0638 4.95333 14.4556 5.34258C14.8474 5.73184 14.8484 6.36398 14.4579 6.75451L8.80101 12.4114C8.41049 12.8019 7.7763 12.8029 7.38452 12.4137L1.70939 6.77513C1.3176 6.38587 1.31658 5.75373 1.70711 5.36321Z" fill="#8E99A4"/>
</g>
<defs>
<clipPath id="clip0_965_9448">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 633 B

5
src/icons/Audio.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.97991 1.48403L4 4.80062L1 4.80062C0.447715 4.80062 0 5.24834 0 5.80062V10.2006C0 10.7529 0.447714 11.2006 0.999999 11.2006L4 11.2006L7.97991 14.5172C8.30557 14.7886 8.8 14.557 8.8 14.1331V1.86814C8.8 1.44422 8.30557 1.21265 7.97991 1.48403Z" fill="white"/>
<path d="M14.1258 2.79107C13.8998 2.50044 13.4809 2.44808 13.1903 2.67413C12.9 2.89992 12.8475 3.3181 13.0726 3.6087L13.0731 3.60935L13.0738 3.61021L13.0829 3.62231C13.0917 3.63418 13.1059 3.65355 13.1248 3.68011C13.1625 3.73326 13.2187 3.81496 13.2872 3.92256C13.4243 4.13812 13.6097 4.45554 13.7955 4.85371C14.169 5.65407 14.5329 6.75597 14.5329 8.00036C14.5329 9.24475 14.169 10.3466 13.7955 11.147C13.6097 11.5452 13.4243 11.8626 13.2872 12.0782C13.2187 12.1858 13.1625 12.2675 13.1248 12.3206C13.1059 12.3472 13.0917 12.3665 13.0829 12.3784L13.0738 12.3905L13.0731 12.3914L13.0725 12.3921C12.8475 12.6827 12.9 13.1008 13.1903 13.3266C13.4809 13.5526 13.8998 13.5003 14.1258 13.2097L13.629 12.8232C14.1258 13.2096 14.1258 13.2097 14.1258 13.2097L14.1272 13.2079L14.1291 13.2055L14.1346 13.1982L14.1523 13.1748C14.1669 13.1552 14.187 13.1277 14.2119 13.0926C14.2617 13.0225 14.3305 12.9221 14.4121 12.794C14.5749 12.5381 14.7895 12.1698 15.0037 11.7109C15.4302 10.7969 15.8663 9.49883 15.8663 8.00036C15.8663 6.50189 15.4302 5.20379 15.0037 4.28987C14.7895 3.83089 14.5749 3.4626 14.4121 3.20673C14.3305 3.07862 14.2617 2.97818 14.2119 2.90811C14.187 2.87306 14.1669 2.84556 14.1523 2.82596L14.1346 2.80249L14.1291 2.79525L14.1272 2.79278L14.1264 2.79183C14.1264 2.79183 14.1258 2.79107 13.5996 3.20036L14.1258 2.79107Z" fill="white"/>
<path d="M11.7264 5.19121C11.5004 4.90058 11.0815 4.84823 10.7909 5.07427C10.501 5.29973 10.4482 5.71698 10.6722 6.00752L10.6745 6.01057C10.6775 6.01457 10.6831 6.02223 10.691 6.03338C10.7069 6.05572 10.7318 6.09189 10.7628 6.14057C10.8249 6.23827 10.9103 6.38426 10.9961 6.56815C11.1696 6.93993 11.3335 7.44183 11.3335 8.00051C11.3335 8.55918 11.1696 9.06108 10.9961 9.43287C10.9103 9.61675 10.8249 9.76275 10.7628 9.86045C10.7318 9.90912 10.7069 9.94529 10.691 9.96763C10.6831 9.97879 10.6775 9.98645 10.6745 9.99044L10.6722 9.9935C10.4482 10.284 10.501 10.7013 10.7909 10.9267C11.0815 11.1528 11.5004 11.1004 11.7264 10.8098L11.2002 10.4005C11.7264 10.8098 11.7264 10.8098 11.7264 10.8098L11.7276 10.8083L11.7291 10.8064L11.7329 10.8014L11.7439 10.7868C11.7526 10.7751 11.7642 10.7593 11.7781 10.7396C11.806 10.7004 11.8436 10.6455 11.8876 10.5763C11.9755 10.4383 12.0901 10.2414 12.2043 9.99672C12.4308 9.51136 12.6669 8.81326 12.6669 8.00051C12.6669 7.18775 12.4308 6.48965 12.2043 6.0043C12.0901 5.75961 11.9755 5.56275 11.8876 5.42473C11.8436 5.35555 11.806 5.30065 11.7781 5.26138C11.7642 5.24173 11.7526 5.22596 11.7439 5.21422L11.7329 5.19964L11.7291 5.19465L11.7276 5.19274L11.727 5.19193C11.727 5.19193 11.7264 5.19121 11.2002 5.60051L11.7264 5.19121Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

3
src/icons/Developer.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3C2.23858 3 0 5.23858 0 8C0 10.7614 2.23858 13 5 13H11C13.7614 13 16 10.7614 16 8C16 5.23858 13.7614 3 11 3H5ZM8 8C8 9.65685 6.65685 11 5 11C3.34315 11 2 9.65685 2 8C2 6.34315 3.34315 5 5 5C6.65685 5 8 6.34315 8 8Z" fill="#A9B2BC"/>
</svg>

After

Width:  |  Height:  |  Size: 388 B

74
src/useMediaHandler.js Normal file
View file

@ -0,0 +1,74 @@
import { useState, useEffect, useCallback } from "react";
let audioOutput;
export function useMediaHandler(client) {
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
useState(() => {
const mediaHandler = client.getMediaHandler();
return {
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs: [],
videoInputs: [],
};
});
useEffect(() => {
const mediaHandler = client.getMediaHandler();
function updateDevices() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);
setState((prevState) => ({
audioInput: mediaHandler.audioInput,
videoInput: mediaHandler.videoInput,
audioInputs,
videoInputs,
}));
});
}
updateDevices();
mediaHandler.on("local_streams_changed", updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => {
mediaHandler.removeListener("local_streams_changed", updateDevices);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
};
}, []);
const setAudioInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client.getMediaHandler().setAudioInput(deviceId);
},
[client]
);
const setVideoInput = useCallback(
(deviceId) => {
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
client.getMediaHandler().setVideoInput(deviceId);
},
[client]
);
return {
audioInput,
audioInputs,
setAudioInput,
videoInput,
videoInputs,
setVideoInput,
};
}

124
yarn.lock
View file

@ -337,6 +337,32 @@
"@react-aria/utils" "^3.10.0"
"@react-types/shared" "^3.10.0"
"@react-aria/label@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.2.1.tgz#e6562259e6b17e3856c4c3e0060903cf705d094b"
integrity sha512-QZ5/dpJKRjB1JtFZfOVd5GUiCpA2yMgmNA6ky6jT5XNAo7H14QqGRFUGDTLAQYGd+Bc3s+NayOT3NKUYur/3Xw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.10.0"
"@react-types/label" "^3.5.0"
"@react-types/shared" "^3.10.0"
"@react-aria/listbox@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.4.0.tgz#384f72f544540b53cb56f5734949aff6f3be723e"
integrity sha512-Tc6JAPHrNKbjFMOCI50YHFBltSxBc84CaLIQdVo4c9KYiwgoAy1ULeSnRyp4ru3qpnffJZEUCNWD+864+MZVEQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.4.1"
"@react-aria/interactions" "^3.7.0"
"@react-aria/label" "^3.2.1"
"@react-aria/selection" "^3.7.0"
"@react-aria/utils" "^3.10.0"
"@react-stately/collections" "^3.3.3"
"@react-stately/list" "^3.4.0"
"@react-types/listbox" "^3.2.1"
"@react-types/shared" "^3.10.0"
"@react-aria/menu@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.3.0.tgz#09364a306b3b0dec7f3cf532bfa184a1f4e26da7"
@ -369,6 +395,25 @@
"@react-types/overlays" "^3.5.1"
dom-helpers "^3.3.1"
"@react-aria/select@^3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.6.0.tgz#d5750614de36af7b26c6d2285bf996647818f8cc"
integrity sha512-jHLyeiy1iR1qaoFdJpQa2V7RL4Nb9JfVDNHNbgp8I5peoU+2oIN34NbqHTnsKlOBfhBRih2PnLogT2Iw4FZ3+Q==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/i18n" "^3.3.3"
"@react-aria/interactions" "^3.7.0"
"@react-aria/label" "^3.2.1"
"@react-aria/listbox" "^3.4.0"
"@react-aria/menu" "^3.3.0"
"@react-aria/selection" "^3.7.0"
"@react-aria/utils" "^3.10.0"
"@react-aria/visually-hidden" "^3.2.3"
"@react-stately/select" "^3.1.3"
"@react-types/button" "^3.4.1"
"@react-types/select" "^3.5.0"
"@react-types/shared" "^3.10.0"
"@react-aria/selection@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.7.0.tgz#77dc483dca82303f20b8567bfac24a2e57bade6d"
@ -390,6 +435,22 @@
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/tabs@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.1.0.tgz#e64b17a592610195466026d7866bde0b32784e69"
integrity sha512-3mU8UclpGVI7muLVTXlJVgHn7RJ+eyBWxPzlgFPBR35acdBrhIt1hBv7PGTAhCg6Zj75CQr07BM/Kdi1WWOP5g==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.0"
"@react-aria/i18n" "^3.3.3"
"@react-aria/interactions" "^3.7.0"
"@react-aria/selection" "^3.7.0"
"@react-aria/utils" "^3.10.0"
"@react-stately/list" "^3.4.0"
"@react-stately/tabs" "^3.0.1"
"@react-types/shared" "^3.10.0"
"@react-types/tabs" "^3.0.1"
"@react-aria/utils@^3.10.0", "@react-aria/utils@^3.8.2", "@react-aria/utils@^3.9.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.10.0.tgz#2f6f0b0ccede17241fca1cbd76978e1bf8f5a2b0"
@ -464,6 +525,17 @@
"@babel/runtime" "^7.6.2"
"@react-types/shared" "^3.8.0"
"@react-stately/list@^3.3.0", "@react-stately/list@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.4.0.tgz#34ef0645f126efac14dbdcf80ca767dea7a87c3d"
integrity sha512-nqVuECSySZU79lF53+YMCl+N1krCoulYNSIohSsply8MN54gJsNHKOKWnhIx05Cdw6hz1rHmdSJ+/sRHhVvBSw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/collections" "^3.3.3"
"@react-stately/selection" "^3.8.0"
"@react-stately/utils" "^3.3.0"
"@react-types/shared" "^3.10.0"
"@react-stately/menu@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.2.3.tgz#eb58e3cfc941d49637bac04aa474935f08bc7215"
@ -484,6 +556,20 @@
"@react-stately/utils" "^3.2.2"
"@react-types/overlays" "^3.5.1"
"@react-stately/select@^3.1.3":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.1.3.tgz#539340e9ccdf8d0b331d289f18f1bb5c81ab3655"
integrity sha512-r0M2gcyyfo7vDDZGsOb64XQlVHtNQl+3mId3gYA46sHEu81C8Lhy4YSPZjItppnCLigBlm88hISl/i0e+XBx8g==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/collections" "^3.3.3"
"@react-stately/list" "^3.3.0"
"@react-stately/menu" "^3.2.3"
"@react-stately/selection" "^3.7.0"
"@react-stately/utils" "^3.2.2"
"@react-types/select" "^3.3.1"
"@react-types/shared" "^3.8.0"
"@react-stately/selection@^3.7.0", "@react-stately/selection@^3.8.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.8.0.tgz#c5292d9b0e67cb48b1bfabd050da57949950e431"
@ -494,6 +580,16 @@
"@react-stately/utils" "^3.3.0"
"@react-types/shared" "^3.10.0"
"@react-stately/tabs@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.0.1.tgz#7df000f8c5c14b3bf973348c491e12bcfcafe49d"
integrity sha512-XhF/5mt8eme3mu0+4nC7Du+e5OWSu0W8SeKfbH9JmTWTCayZpPtui68nRStJK6OkgHs28gA+j55RSsTqT/N1Fg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/list" "^3.3.0"
"@react-stately/utils" "^3.2.2"
"@react-types/tabs" "^3.0.1"
"@react-stately/toggle@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.2.3.tgz#a4de6edc16982990492c6c557e5194f46dacc809"
@ -544,6 +640,20 @@
"@react-types/overlays" "^3.5.1"
"@react-types/shared" "^3.8.0"
"@react-types/label@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@react-types/label/-/label-3.5.0.tgz#c7093871f42c62e1b5523f61a0856a2f58d4cf2a"
integrity sha512-a9lpQUyV4XwsZv0gV1jPjPWicSSa+DRliuXLTwORirxNLF0kMk89DLYf0a9CZhiEniJYqoqR3laJDvLAFW1x/Q==
dependencies:
"@react-types/shared" "^3.9.0"
"@react-types/listbox@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.2.1.tgz#881bbc9690343f35fe08a99526a90618f53328bf"
integrity sha512-uBYx5BgL8gyH62UhSXAFyFDwAD4ALcK5gjOk+p/vWsFm0vvmtutALkb3yYjDQvwdI89pSZDjN4j7QChCmlNcmQ==
dependencies:
"@react-types/shared" "^3.8.0"
"@react-types/menu@^3.3.0":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.4.1.tgz#42f58ce3b79b844441627c5cd3126705b3b00063"
@ -559,11 +669,25 @@
dependencies:
"@react-types/shared" "^3.8.0"
"@react-types/select@^3.3.1", "@react-types/select@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.5.0.tgz#50ae1e7b14925189ea45c8322a776fd40473d31e"
integrity sha512-XdLS/kvvlOZbVP/wn8tX5iAL0kpND3ZSea8KXG3EkwIw8sn1xcd8tYx7TkdF89IdNbg2pmzn9stOv9RUbC8MoQ==
dependencies:
"@react-types/shared" "^3.10.0"
"@react-types/shared@^3.10.0", "@react-types/shared@^3.8.0", "@react-types/shared@^3.9.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.10.0.tgz#bdafed2ebcd31149c178312252dda0babde316d0"
integrity sha512-B1gTRpE5qkSpfGxw8BHeOwvBPP3gnfKnzPHV0FJQHtgJ46oJS64WyloDAp1D9cLVsFHaI6s/HviXL51kVce2ww==
"@react-types/tabs@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.0.1.tgz#a32931e95303d4442e51d4c687d4e154654014f6"
integrity sha512-GvPVU9GAqImHFhU+Do+pdGK/vZA4kqA699Gly1V95DUmtdG3GSwTnwlvM/Sy80/F9fKZDGokZnQmBFo8MFZyIw==
dependencies:
"@react-types/shared" "^3.8.0"
"@sentry/browser@6.13.3":
version "6.13.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.3.tgz#d4511791b1e484ad48785eba3bce291fdf115c1e"