Start using the new modal component

This attempts to converge all our modals on the new modal component while changing their designs as little as possible. This should reduce the bundle size a bit and make the app generally feel like it's converging on the new designs, even though individual modals still remain to be revamped.
This commit is contained in:
Robin
2023-09-17 14:35:35 -04:00
parent f609ec3f4c
commit 9db21e024e
23 changed files with 502 additions and 764 deletions

View File

@@ -32,7 +32,6 @@
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-visually-hidden": "^1.0.3",
"@react-aria/button": "^3.3.4",
"@react-aria/dialog": "^3.1.4",
"@react-aria/focus": "^3.5.0",
"@react-aria/menu": "^3.3.0",
"@react-aria/overlays": "^3.7.3",
@@ -42,7 +41,6 @@
"@react-aria/utils": "^3.10.0",
"@react-spring/web": "^9.4.4",
"@react-stately/collections": "^3.3.4",
"@react-stately/overlays": "^3.1.3",
"@react-stately/select": "^3.1.3",
"@react-stately/tooltip": "^3.0.5",
"@react-stately/tree": "^3.2.0",
@@ -84,6 +82,7 @@
"devDependencies": {
"@babel/core": "^7.16.5",
"@react-spring/rafz": "^9.7.3",
"@react-types/dialog": "^3.5.5",
"@sentry/vite-plugin": "^0.3.0",
"@storybook/react": "^6.5.0-alpha.5",
"@testing-library/jest-dom": "^5.16.5",

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,77 +14,204 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.modalOverlay {
.overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(23, 25, 28, 0.5);
display: flex;
align-items: center;
justify-content: center;
inset: 0;
background: rgba(3, 12, 27, 0.528);
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.dialogOverlay[data-state="open"] {
animation: fade-in 200ms;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dialogOverlay[data-state="closed"] {
animation: fade-out 130ms;
}
.modal {
background: var(--cpd-color-bg-subtle-secondary);
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
border-radius: 8px;
max-width: 90vw;
width: 600px;
position: fixed;
z-index: 101;
display: flex;
flex-direction: column;
}
.modalHeader {
display: flex;
justify-content: space-between;
padding: 34px 32px 0 32px;
.dialog {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
inline-size: 520px;
max-inline-size: 90%;
max-block-size: 600px;
}
.modalHeader h3 {
font-weight: 600;
font-size: var(--font-size-title);
margin: 0;
@keyframes zoom-in {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(80%);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
}
.closeButton {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
padding: 0;
border: none;
cursor: pointer;
@keyframes zoom-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
to {
opacity: 0;
transform: translate(-50%, -50%) scale(80%);
}
}
.dialog[data-state="open"] {
animation: zoom-in 200ms;
}
.dialog[data-state="closed"] {
animation: zoom-out 130ms;
}
@media (prefers-reduced-motion) {
.dialog[data-state="open"] {
animation-name: fade-in;
}
.dialog[data-state="closed"] {
animation-name: fade-out;
}
}
.content {
padding: 24px 32px;
}
.content p {
margin-top: 0;
}
@media (max-width: 799px) {
.modalHeader {
display: flex;
justify-content: space-between;
padding: 32px 20px 0 20px;
flex-direction: column;
overflow: hidden;
}
.modal.mobileFullScreen {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
border-radius: 0;
.dialog .content {
flex-grow: 1;
background: var(--cpd-color-bg-canvas-default);
}
.drawer .content {
overflow: auto;
}
.drawer {
background: var(--cpd-color-bg-canvas-default);
inset-block-end: 0;
inset-inline: max(0px, calc((100% - 520px) / 2));
max-block-size: 90%;
border-start-start-radius: 20px;
border-start-end-radius: 20px;
/* Drawer handle comes in the Android style by default */
--handle-block-size: 4px;
--handle-inline-size: 32px;
--handle-inset-block-start: var(--cpd-space-4x);
--handle-inset-block-end: var(--cpd-space-4x);
}
body[data-platform="ios"] .drawer {
--handle-block-size: 5px;
--handle-inline-size: 36px;
--handle-inset-block-start: var(--cpd-space-1-5x);
--handle-inset-block-end: calc(var(--cpd-space-1x) / 4);
}
.close {
cursor: pointer;
color: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
padding: var(--cpd-space-1x);
background: var(--cpd-color-bg-subtle-secondary);
border: none;
}
.close svg {
display: block;
}
@media (hover: hover) {
.close:hover {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
}
}
.close:active {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
}
.header {
background: var(--cpd-color-bg-subtle-secondary);
display: grid;
}
.dialog .header {
padding-block-start: var(--cpd-space-4x);
grid-template-columns:
var(--cpd-space-10x) 1fr minmax(var(--cpd-space-6x), auto)
var(--cpd-space-4x);
grid-template-rows: auto minmax(var(--cpd-space-4x), auto);
/* TODO: Support tabs */
grid-template-areas: ". title close ." "tabs tabs tabs tabs";
align-items: center;
}
.dialog .header h2 {
grid-area: title;
margin: 0;
}
.drawer .header {
grid-template-areas: "tabs";
position: relative;
}
.close {
grid-area: close;
}
.dialog .body {
padding-inline: var(--cpd-space-10x);
padding-block: var(--cpd-space-10x) var(--cpd-space-12x);
overflow: auto;
}
.drawer .body {
padding-inline: var(--cpd-space-4x);
padding-block: var(--cpd-space-9x) var(--cpd-space-10x);
}
.handle {
content: "";
position: absolute;
block-size: var(--handle-block-size);
inset-inline: calc((100% - var(--handle-inline-size)) / 2);
inset-block-start: var(--handle-inset-block-start);
background: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,123 +14,128 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable jsx-a11y/no-autofocus */
import { useRef, useMemo, ReactNode } from "react";
import {
useOverlay,
usePreventScroll,
useModal,
OverlayContainer,
OverlayProps,
} from "@react-aria/overlays";
import {
OverlayTriggerState,
useOverlayTriggerState,
} from "@react-stately/overlays";
import { useDialog } from "@react-aria/dialog";
import { FocusScope } from "@react-aria/focus";
import { useButton } from "@react-aria/button";
import classNames from "classnames";
import { ReactNode, useCallback } from "react";
import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next";
import {
Root as DialogRoot,
Portal as DialogPortal,
Overlay as DialogOverlay,
Content as DialogContent,
Title as DialogTitle,
Close as DialogClose,
} from "@radix-ui/react-dialog";
import { Drawer } from "vaul";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { ReactComponent as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
import classNames from "classnames";
import { Heading } from "@vector-im/compound-web";
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
import styles from "./Modal.module.css";
import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass";
export interface ModalProps extends OverlayProps, AriaDialogProps {
// TODO: Support tabs
export interface ModalProps extends AriaDialogProps {
title: string;
children: ReactNode;
className?: string;
mobileFullScreen?: boolean;
onClose: () => void;
/**
* The controlled open state of the modal.
*/
// An option to leave the open state uncontrolled is intentionally not
// provided, since modals are always opened due to external triggers, and it
// is the author's belief that controlled components lead to more obvious code
open: boolean;
/**
* Callback for when the user dismisses the modal. If undefined, the modal
* will be non-dismissable.
*/
onDismiss?: () => void;
}
/**
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
* and a dialog box on desktop.
*/
export function Modal({
title,
children,
className,
mobileFullScreen,
onClose,
open,
onDismiss,
...rest
}: ModalProps) {
const { t } = useTranslation();
const modalRef = useRef(null);
const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose },
modalRef
);
usePreventScroll();
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(rest, modalRef);
const closeButtonRef = useRef(null);
const { buttonProps: closeButtonProps } = useButton(
{
onPress: () => onClose(),
const touchscreen = useMediaQuery("(hover: none)");
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) onDismiss?.();
},
closeButtonRef
[onDismiss]
);
if (touchscreen) {
return (
<OverlayContainer>
<div className={styles.modalOverlay} {...underlayProps}>
<FocusScope contain restoreFocus autoFocus>
<div
{...overlayProps}
{...dialogProps}
{...modalProps}
ref={modalRef}
className={classNames(
styles.modal,
{ [styles.mobileFullScreen]: mobileFullScreen },
className
)}
<Drawer.Root
open={open}
onOpenChange={onOpenChange}
dismissible={onDismiss !== undefined}
>
<div className={styles.modalHeader}>
<h3 {...titleProps}>{title}</h3>
<button
{...closeButtonProps}
ref={closeButtonRef}
className={styles.closeButton}
<Drawer.Portal>
<Drawer.Overlay className={styles.overlay} />
<Drawer.Content
className={classNames(className, styles.modal, styles.drawer)}
{...rest}
>
<div className={styles.content}>
<div className={styles.header}>
<div className={styles.handle} />
<VisuallyHidden asChild>
<Drawer.Title>{title}</Drawer.Title>
</VisuallyHidden>
</div>
<div className={styles.body}>{children}</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
} else {
return (
<DialogRoot open={open} onOpenChange={onOpenChange}>
<DialogPortal>
<DialogOverlay
className={classNames(styles.overlay, styles.dialogOverlay)}
/>
<DialogContent asChild {...rest}>
<Glass
frosted
className={classNames(className, styles.modal, styles.dialog)}
>
<div className={styles.content}>
<div className={styles.header}>
<DialogTitle asChild>
<Heading as="h2" weight="semibold" size="md">
{title}
</Heading>
</DialogTitle>
{onDismiss !== undefined && (
<DialogClose
className={styles.close}
data-testid="modal_close"
title={t("Close")}
aria-label={t("Close")}
>
<CloseIcon />
</button>
<CloseIcon width={20} height={20} />
</DialogClose>
)}
</div>
{children}
<div className={styles.body}>{children}</div>
</div>
</FocusScope>
</div>
</OverlayContainer>
</Glass>
</DialogContent>
</DialogPortal>
</DialogRoot>
);
}
interface ModalContentProps {
children: ReactNode;
className?: string;
}
export function ModalContent({
children,
className,
...rest
}: ModalContentProps) {
return (
<div className={classNames(styles.content, className)} {...rest}>
{children}
</div>
);
}
export function useModalTriggerState(): {
modalState: OverlayTriggerState;
modalProps: { isOpen: boolean; onClose: () => void };
} {
const modalState = useOverlayTriggerState({});
const modalProps = useMemo(
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
[modalState]
);
return { modalState, modalProps };
}

View File

@@ -1,217 +0,0 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.overlay {
position: fixed;
z-index: 100;
inset: 0;
background: rgba(3, 12, 27, 0.528);
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.dialogOverlay[data-state="open"] {
animation: fade-in 200ms;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dialogOverlay[data-state="closed"] {
animation: fade-out 130ms;
}
.modal {
position: fixed;
z-index: 101;
display: flex;
flex-direction: column;
}
.dialog {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
inline-size: 520px;
max-inline-size: 90%;
max-block-size: 600px;
}
@keyframes zoom-in {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(80%);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
}
@keyframes zoom-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
to {
opacity: 0;
transform: translate(-50%, -50%) scale(80%);
}
}
.dialog[data-state="open"] {
animation: zoom-in 200ms;
}
.dialog[data-state="closed"] {
animation: zoom-out 130ms;
}
@media (prefers-reduced-motion) {
.dialog[data-state="open"] {
animation-name: fade-in;
}
.dialog[data-state="closed"] {
animation-name: fade-out;
}
}
.content {
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog .content {
flex-grow: 1;
background: var(--cpd-color-bg-canvas-default);
}
.drawer .content {
overflow: auto;
}
.drawer {
background: var(--cpd-color-bg-canvas-default);
inset-block-end: 0;
inset-inline: max(0px, calc((100% - 520px) / 2));
max-block-size: 90%;
border-start-start-radius: 20px;
border-start-end-radius: 20px;
/* Drawer handle comes in the Android style by default */
--handle-block-size: 4px;
--handle-inline-size: 32px;
--handle-inset-block-start: var(--cpd-space-4x);
--handle-inset-block-end: var(--cpd-space-4x);
}
body[data-platform="ios"] .drawer {
--handle-block-size: 5px;
--handle-inline-size: 36px;
--handle-inset-block-start: var(--cpd-space-1-5x);
--handle-inset-block-end: calc(var(--cpd-space-1x) / 4);
}
.close {
cursor: pointer;
color: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
padding: var(--cpd-space-1x);
background: var(--cpd-color-bg-subtle-secondary);
border: none;
}
.close svg {
display: block;
}
@media (hover: hover) {
.close:hover {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
}
}
.close:active {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
}
.header {
background: var(--cpd-color-bg-subtle-secondary);
display: grid;
}
.dialog .header {
padding-block-start: var(--cpd-space-4x);
grid-template-columns:
var(--cpd-space-10x) 1fr minmax(var(--cpd-space-6x), auto)
var(--cpd-space-4x);
grid-template-rows: auto minmax(var(--cpd-space-4x), auto);
/* TODO: Support tabs */
grid-template-areas: ". title close ." "tabs tabs tabs tabs";
align-items: center;
}
.dialog .header h2 {
grid-area: title;
margin: 0;
}
.drawer .header {
grid-template-areas: "tabs";
position: relative;
}
.close {
grid-area: close;
}
.dialog .body {
padding-inline: var(--cpd-space-10x);
padding-block: var(--cpd-space-10x) var(--cpd-space-12x);
overflow: auto;
}
.drawer .body {
padding-inline: var(--cpd-space-4x);
padding-block: var(--cpd-space-9x) var(--cpd-space-10x);
}
.handle {
content: "";
position: absolute;
block-size: var(--handle-block-size);
inset-inline: calc((100% - var(--handle-inline-size)) / 2);
inset-block-start: var(--handle-inset-block-start);
background: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
}

View File

@@ -1,141 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ReactNode, useCallback } from "react";
import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next";
import {
Root as DialogRoot,
Portal as DialogPortal,
Overlay as DialogOverlay,
Content as DialogContent,
Title as DialogTitle,
Close as DialogClose,
} from "@radix-ui/react-dialog";
import { Drawer } from "vaul";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { ReactComponent as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
import classNames from "classnames";
import { Heading } from "@vector-im/compound-web";
import styles from "./NewModal.module.css";
import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass";
// TODO: Support tabs
export interface ModalProps extends AriaDialogProps {
title: string;
children: ReactNode;
className?: string;
/**
* The controlled open state of the modal.
*/
// An option to leave the open state uncontrolled is intentionally not
// provided, since modals are always opened due to external triggers, and it
// is the author's belief that controlled components lead to more obvious code
open: boolean;
/**
* Callback for when the user dismisses the modal. If undefined, the modal
* will be non-dismissable.
*/
onDismiss?: () => void;
}
/**
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
* and a dialog box on desktop.
*/
export function Modal({
title,
children,
className,
open,
onDismiss,
...rest
}: ModalProps) {
const { t } = useTranslation();
const touchscreen = useMediaQuery("(hover: none)");
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) onDismiss?.();
},
[onDismiss]
);
if (touchscreen) {
return (
<Drawer.Root
open={open}
onOpenChange={onOpenChange}
dismissible={onDismiss !== undefined}
>
<Drawer.Portal>
<Drawer.Overlay className={styles.overlay} />
<Drawer.Content
className={classNames(className, styles.modal, styles.drawer)}
{...rest}
>
<div className={styles.content}>
<div className={styles.header}>
<div className={styles.handle} />
<VisuallyHidden asChild>
<Drawer.Title>{title}</Drawer.Title>
</VisuallyHidden>
</div>
<div className={styles.body}>{children}</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
} else {
return (
<DialogRoot open={open} onOpenChange={onOpenChange}>
<DialogPortal>
<DialogOverlay
className={classNames(styles.overlay, styles.dialogOverlay)}
/>
<DialogContent asChild {...rest}>
<Glass
frosted
className={classNames(className, styles.modal, styles.dialog)}
>
<div className={styles.content}>
<div className={styles.header}>
<DialogTitle asChild>
<Heading as="h2" weight="semibold" size="md">
{title}
</Heading>
</DialogTitle>
{onDismiss !== undefined && (
<DialogClose
className={styles.close}
data-testid="modal_close"
aria-label={t("Close")}
>
<CloseIcon width={20} height={20} />
</DialogClose>
)}
</div>
<div className={styles.body}>{children}</div>
</div>
</Glass>
</DialogContent>
</DialogPortal>
</DialogRoot>
);
}
}

View File

@@ -19,7 +19,6 @@ import { useHistory, useLocation } from "react-router-dom";
import { useClientLegacy } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal";
import { UserMenu } from "./UserMenu";
@@ -32,7 +31,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
const history = useHistory();
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const onDismissSettingsModal = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
@@ -41,11 +44,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
switch (value) {
case "user":
setDefaultSettingsTab("profile");
modalState.open();
setSettingsModalOpen(true);
break;
case "settings":
setDefaultSettingsTab("audio");
modalState.open();
setSettingsModalOpen(true);
break;
case "logout":
logout?.();
@@ -55,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
break;
}
},
[history, location, logout, modalState]
[history, location, logout, setSettingsModalOpen]
);
const userName = client?.getUserIdLocalpart() ?? "";
@@ -70,11 +73,12 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
userId={client?.getUserId() ?? ""}
displayName={displayName || (userName ? userName.replace("@", "") : "")}
/>
{modalState.isOpen && client && (
{client && (
<SettingsModal
client={client}
defaultTab={defaultSettingsTab}
{...modalProps}
open={settingsModalOpen}
onDismiss={onDismissSettingsModal}
/>
)}
</>

View File

@@ -17,36 +17,29 @@ limitations under the License.
import { PressEvent } from "@react-types/shared";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal";
import { Modal } from "../Modal";
import { Button } from "../button";
import { FieldRow } from "../input/Input";
import styles from "./JoinExistingCallModal.module.css";
interface Props {
open: boolean;
onDismiss: () => void;
onJoin: (e: PressEvent) => void;
onClose: () => void;
// TODO: add used parameters for <Modal>
[index: string]: unknown;
}
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
const { t } = useTranslation();
return (
<Modal
title={t("Join existing call?")}
isDismissable
{...rest}
onClose={onClose}
>
<ModalContent>
<Modal title={t("Join existing call?")} open={open} onDismiss={onDismiss}>
<p>{t("This call already exists, would you like to join?")}</p>
<FieldRow rightAlign className={styles.buttons}>
<Button onPress={onClose}>{t("No")}</Button>
<Button onPress={onDismiss}>{t("No")}</Button>
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
{t("Yes, join call")}
</Button>
</FieldRow>
</ModalContent>
</Modal>
);
}

View File

@@ -33,7 +33,6 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
import { CallList } from "./CallList";
import { UserMenuContainer } from "../UserMenuContainer";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption, Title } from "../typography/Typography";
import { Form } from "../form/Form";
@@ -56,7 +55,12 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const [optInAnalytics] = useOptInAnalytics();
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen]
);
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
@@ -93,7 +97,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
setLoading(false);
setError(undefined);
modalState.open();
setJoinExistingCallModalOpen(true);
} else {
console.error(error);
setLoading(false);
@@ -101,7 +105,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
}
});
},
[client, history, modalState, callType, e2eeEnabled]
[client, history, setJoinExistingCallModalOpen, callType, e2eeEnabled]
);
const recentRooms = useGroupCallRooms(client);
@@ -175,9 +179,11 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
)}
</main>
</div>
{modalState.isOpen && (
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
)}
<JoinExistingCallModal
onJoin={onJoinExistingRoom}
open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
</>
);
}

View File

@@ -30,7 +30,6 @@ import {
sanitiseRoomNameInput,
} from "../matrix-utils";
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { useRecaptcha } from "../auth/useRecaptcha";
import { Body, Caption, Link } from "../typography/Typography";
@@ -55,7 +54,12 @@ export const UnauthenticatedView: FC = () => {
const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const { modalState, modalProps } = useModalTriggerState();
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen]
);
const [onFinished, setOnFinished] = useState<() => void>();
const history = useHistory();
const { t } = useTranslation();
@@ -110,7 +114,7 @@ export const UnauthenticatedView: FC = () => {
});
setLoading(false);
modalState.open();
setJoinExistingCallModalOpen(true);
return;
} else {
throw error;
@@ -139,7 +143,7 @@ export const UnauthenticatedView: FC = () => {
execute,
history,
callType,
modalState,
setJoinExistingCallModalOpen,
setClient,
e2eeEnabled,
]
@@ -235,8 +239,12 @@ export const UnauthenticatedView: FC = () => {
</Body>
</footer>
</div>
{modalState.isOpen && onFinished && (
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
{onFinished && (
<JoinExistingCallModal
onJoin={onFinished}
open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
)}
</>
);

View File

@@ -200,8 +200,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
* default because it may involve requesting additional permissions from the
* user.
*/
export const useMediaDeviceNames = (context: MediaDevices) =>
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
useEffect(() => {
if (enabled) {
context.startUsingDeviceNames();
return context.stopUsingDeviceNames;
}, [context]);
}
}, [context, enabled]);

View File

@@ -45,7 +45,6 @@ import {
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
@@ -286,12 +285,15 @@ export function GroupCallView({
const joinRule = useJoinRule(rtcSession.room);
const { modalState: shareModalState, modalProps: shareModalProps } =
useModalTriggerState();
const [shareModalOpen, setShareModalOpen] = useState(false);
const onDismissShareModal = useCallback(
() => setShareModalOpen(false),
[setShareModalOpen]
);
const onShareClickFn = useCallback(
() => shareModalState.open(),
[shareModalState]
() => setShareModalOpen(true),
[setShareModalOpen]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -311,8 +313,12 @@ export function GroupCallView({
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
}
const shareModal = shareModalState.isOpen && (
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
const shareModal = (
<ShareModal
roomId={rtcSession.room.roomId}
open={shareModalOpen}
onDismiss={onDismissShareModal}
/>
);
if (isJoined) {

View File

@@ -30,7 +30,6 @@ import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
@@ -51,7 +50,6 @@ import {
VideoGrid,
} from "../video-grid/VideoGrid";
import { useShowConnectionStats } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
@@ -313,25 +311,20 @@ export function InCallView({
);
};
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(rtcSession.room.roomId);
const rageshakeRequestModalProps = useRageshakeRequestModal(
rtcSession.room.roomId
);
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const toggleScreensharing = useCallback(async () => {
exitFullscreen();
@@ -442,19 +435,13 @@ export function InCallView({
show={showInspector}
/>
)*/}
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomId={rtcSession.room.roomId}
/>
)}
{settingsModalState.isOpen && (
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
<SettingsModal
client={client}
roomId={rtcSession.room.roomId}
{...settingsModalProps}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)}
</div>
);
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Modal, ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
@@ -26,26 +26,25 @@ import { Body } from "../typography/Typography";
interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string;
roomId: string;
onClose: () => void;
open: boolean;
onDismiss: () => void;
}
export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId,
roomId,
...rest
open,
onDismiss,
}) => {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
if (sent) {
rest.onClose();
}
}, [sent, rest]);
if (sent) onDismiss();
}, [sent, onDismiss]);
return (
<Modal title={t("Debug log request")} isDismissable {...rest}>
<ModalContent>
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
<Body>
{t(
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
@@ -70,7 +69,6 @@ export const RageshakeRequestModal: FC<Props> = ({
<ErrorMessage error={error} />
</FieldRow>
)}
</ModalContent>
</Modal>
);
};

View File

@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.inviteModal {
max-width: 413px;
}
.copyButton {
width: 100%;
}

View File

@@ -17,35 +17,30 @@ limitations under the License.
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Modal } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props extends Omit<ModalProps, "title" | "children"> {
interface Props {
roomId: string;
open: boolean;
onDismiss: () => void;
}
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
export const ShareModal: FC<Props> = ({ roomId, open, onDismiss }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId);
return (
<Modal
title={t("Share this call")}
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
data-testid="modal_inviteLink"
/>
</ModalContent>
</Modal>
);
};

View File

@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useCallback, useMemo, useRef, FC } from "react";
import { useEffect, useCallback, useMemo, useRef, FC, useState } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { OverlayTriggerState } from "@react-stately/overlays";
import { usePreviewTracks } from "@livekit/components-react";
import {
CreateLocalTracksOptions,
@@ -28,7 +27,6 @@ import {
import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar";
import styles from "./VideoPreview.module.css";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/MediaDevicesContext";
@@ -54,20 +52,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
const { client } = useClient();
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const devices = useMediaDevices();
@@ -153,8 +147,12 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
<SettingsButton onPress={openSettings} />
</div>
</>
{settingsModalState.isOpen && client && (
<SettingsModal client={client} {...settingsModalProps} />
{client && (
<SettingsModal
client={client}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)}
</div>
);

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,18 +15,13 @@ limitations under the License.
*/
.settingsModal {
width: 774px;
height: 480px;
block-size: 550px;
}
.settingsModal p {
color: var(--cpd-color-text-secondary);
}
.tabContainer {
padding: 27px 20px;
}
.fieldRowText {
margin-bottom: 0;
}

View File

@@ -51,11 +51,11 @@ import {
} from "../livekit/MediaDevicesContext";
interface Props {
isOpen: boolean;
open: boolean;
onDismiss: () => void;
client: MatrixClient;
roomId?: string;
defaultTab?: string;
onClose: () => void;
}
export const SettingsModal = (props: Props) => {
@@ -119,7 +119,7 @@ export const SettingsModal = (props: Props) => {
);
const devices = useMediaDevices();
useMediaDeviceNames(devices);
useMediaDeviceNames(devices, props.open);
const audioTab = (
<TabItem
@@ -289,10 +289,9 @@ export const SettingsModal = (props: Props) => {
return (
<Modal
title={t("Settings")}
isDismissable
mobileFullScreen
className={styles.settingsModal}
{...props}
open={props.open}
onDismiss={props.onDismiss}
>
<TabContainer
onSelectionChange={onSelectedTabChanged}

View File

@@ -14,20 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useContext, useEffect, useState } from "react";
import {
ComponentProps,
useCallback,
useContext,
useEffect,
useState,
} from "react";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import pako from "pako";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { OverlayTriggerState } from "@react-stately/overlays";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
import { Config } from "../config/Config";
import { ElementCallOpenTelemetry } from "../otel/otel";
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
const gzip = (text: string): Blob => {
// encode as UTF-8
@@ -343,22 +348,12 @@ export function useRageshakeRequest(): (
return sendRageshakeRequest;
}
interface ModalProps {
isOpen: boolean;
onClose: () => void;
}
interface ModalPropsWithId extends ModalProps {
rageshakeRequestId: string;
}
export function useRageshakeRequestModal(roomId: string): {
modalState: OverlayTriggerState;
modalProps: ModalPropsWithId;
} {
const { modalState, modalProps } = useModalTriggerState() as {
modalState: OverlayTriggerState;
modalProps: ModalProps;
};
export function useRageshakeRequestModal(
roomId: string
): ComponentProps<typeof RageshakeRequestModal> {
const [open, setOpen] = useState(false);
const onDismiss = useCallback(() => setOpen(false), [setOpen]);
const { client } = useClient();
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
@@ -374,7 +369,7 @@ export function useRageshakeRequestModal(roomId: string): {
client.getUserId() !== event.getSender()
) {
setRageshakeRequestId(event.getContent().request_id);
modalState.open();
setOpen(true);
}
};
@@ -383,10 +378,12 @@ export function useRageshakeRequestModal(roomId: string): {
return () => {
client.removeListener(ClientEvent.Event, onEvent);
};
}, [modalState.open, roomId, client, modalState]);
}, [setOpen, roomId, client]);
return {
modalState,
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" },
rageshakeRequestId: rageshakeRequestId ?? "",
roomId,
open,
onDismiss,
};
}

View File

@@ -78,32 +78,3 @@ limitations under the License.
padding: 0;
overflow-y: auto;
}
@media (min-width: 800px) {
.tab {
width: 200px;
padding: 0 16px;
}
.tab > * {
margin: 0 12px 0 0;
}
.tabContainer {
width: 100%;
flex-direction: row;
padding: 20px 18px;
box-sizing: border-box;
overflow: hidden;
}
.tabList {
flex-direction: column;
margin-bottom: 0;
gap: 0;
}
.tabPanel {
padding: 0 40px;
}
}

View File

@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ComponentProps, forwardRef, useCallback, useEffect } from "react";
import {
ComponentProps,
forwardRef,
useCallback,
useEffect,
useState,
} from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
@@ -36,7 +42,6 @@ import { Avatar } from "../Avatar";
import styles from "./VideoTile.module.css";
import { useReactiveState } from "../useReactiveState";
import { AudioButton, FullscreenButton } from "../button/Button";
import { useModalTriggerState } from "../Modal";
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
export interface ItemData {
@@ -117,11 +122,16 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
onToggleFullscreen(data.id);
}, [data, onToggleFullscreen]);
const {
modalState: videoTileSettingsModalState,
modalProps: videoTileSettingsModalProps,
} = useModalTriggerState();
const onOptionsPress = videoTileSettingsModalState.open;
const [videoTileSettingsModalOpen, setVideoTileSettingsModalOpen] =
useState(false);
const openVideoTileSettingsModal = useCallback(
() => setVideoTileSettingsModalOpen(true),
[setVideoTileSettingsModalOpen]
);
const closeVideoTileSettingsModal = useCallback(
() => setVideoTileSettingsModalOpen(false),
[setVideoTileSettingsModalOpen]
);
const toolbarButtons: JSX.Element[] = [];
if (!sfuParticipant.isLocal) {
@@ -130,7 +140,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
key="localVolume"
className={styles.button}
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
onPress={onOptionsPress}
onPress={openVideoTileSettingsModal}
/>
);
@@ -208,10 +218,11 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
: Track.Source.ScreenShare
}
/>
{videoTileSettingsModalState.isOpen && !maximised && (
{!maximised && (
<VideoTileSettingsModal
{...videoTileSettingsModalProps}
data={data}
open={videoTileSettingsModalOpen}
onDismiss={closeVideoTileSettingsModal}
/>
)}
</animated.div>

View File

@@ -66,23 +66,21 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
);
};
// TODO: Extend ModalProps
interface Props {
data: ItemData;
onClose: () => void;
open: boolean;
onDismiss: () => void;
}
export const VideoTileSettingsModal = ({ data, onClose, ...rest }: Props) => {
export const VideoTileSettingsModal = ({ data, open, onDismiss }: Props) => {
const { t } = useTranslation();
return (
<Modal
className={styles.videoTileSettingsModal}
title={t("Local volume")}
isDismissable
mobileFullScreen
onClose={onClose}
{...rest}
open={open}
onDismiss={onDismiss}
>
<div className={styles.content}>
<LocalVolume

View File

@@ -2822,17 +2822,6 @@
"@react-stately/toggle" "^3.3.1"
"@react-types/button" "^3.5.1"
"@react-aria/dialog@^3.1.4":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.2.1.tgz#8e004727b7cc6fcde3ab4de2a50c7d06e6b0d7c3"
integrity sha512-q3834JCNXcVSSfiez8R+6OunQzwiaM/sGctklRVBUooo80nJbPhnegumKiYe1Va4Gz7i/aLZwSEeK4AU3GMA9Q==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.6.1"
"@react-aria/utils" "^3.13.1"
"@react-stately/overlays" "^3.3.1"
"@react-types/dialog" "^3.4.1"
"@react-aria/focus@^3.5.0", "@react-aria/focus@^3.6.1":
version "3.6.1"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.6.1.tgz#46478d0919bdc4fedfa1ea115b36f93c055ce8d8"
@@ -3096,7 +3085,7 @@
"@react-types/menu" "^3.6.1"
"@react-types/shared" "^3.13.1"
"@react-stately/overlays@^3.1.3", "@react-stately/overlays@^3.3.1":
"@react-stately/overlays@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.3.1.tgz#91bd3e349a0d223a42a7dc6a72f9503d4c26e954"
integrity sha512-IRaK8x9OnwP6p9sojs39hF4nXNqRTt1qydbQMTw876SU94kG2pdqk/vHe4qdGVEaHB/mZ/8wRMntitQ0C6XFKA==
@@ -3191,13 +3180,13 @@
dependencies:
"@react-types/shared" "^3.13.1"
"@react-types/dialog@^3.4.1":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.4.1.tgz#67394846c3b3348887699619f28939e99d15f221"
integrity sha512-tB/zDF4sR0l8GhTz0hl6bZGAYyxbF7UtTHRVVPW1XSDgjM7nuZiJrWb300S8KICj4933+ZmB5DCuzCzEKjIV6g==
"@react-types/dialog@^3.5.5":
version "3.5.5"
resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.5.5.tgz#bcd8d40bedc4c704161496d4c19a417ecc753b6a"
integrity sha512-XidCDLmbagLQZlnV8QVPhS3a63GdwiSa/0MYsHLDeb81+7P2vc3r+wNgnHWZw64mICWYzyyKxpzV3QpUm4f6+g==
dependencies:
"@react-types/overlays" "^3.6.1"
"@react-types/shared" "^3.13.1"
"@react-types/overlays" "^3.8.2"
"@react-types/shared" "^3.20.0"
"@react-types/label@^3.6.1":
version "3.6.1"
@@ -3228,6 +3217,13 @@
dependencies:
"@react-types/shared" "^3.13.1"
"@react-types/overlays@^3.8.2":
version "3.8.2"
resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.8.2.tgz#1411e0a1626f4140de0ce67835f24a6a18f8d5de"
integrity sha512-HpLYzkNvuvC6nKd06vF9XbcLLv3u55+e7YUFNVpgWq8yVxcnduOcJdRJhPaAqHUl6iVii04mu1GKnCFF8jROyQ==
dependencies:
"@react-types/shared" "^3.20.0"
"@react-types/select@^3.6.1":
version "3.6.1"
resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.6.1.tgz#12c879c2d2c3d03fa741b4e4175df7f57aab452a"
@@ -3245,6 +3241,11 @@
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.19.0.tgz#060e547d6e8c3ec84043d62f61cada1a00df1348"
integrity sha512-h852l8bWhqUxbXIG8vH3ab7gE19nnP3U1kuWf6SNSMvgmqjiRN9jXKPIFxF/PbfdvnXXm0yZSgSMWfUCARF0Cg==
"@react-types/shared@^3.20.0":
version "3.20.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.20.0.tgz#15f0cbe3978831589f083c8e316810669b4fa606"
integrity sha512-lgTO/S/EMIZKU1EKTg8wT0qYP5x/lZTK2Xw6BZZk5c4nn36JYhGCRb/OoR/jBCIeRb2x9yNbwERO6NYVkoQMSw==
"@react-types/tabs@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.1.1.tgz#98c891e13d4a7e4fa3cec8dc8fd4efd3e4f39707"