Fix for interactions outside of modals

This commit is contained in:
Rafee 2024-01-12 14:49:30 -05:00
Родитель 43d533898d
Коммит ad3f123a28
8 изменённых файлов: 226 добавлений и 13 удалений

Просмотреть файл

@ -0,0 +1,161 @@
// import { ButtonHTMLAttributes, CSSProperties, DetailedReactHTMLElement, ReactNode, RefObject, cloneElement, forwardRef, isValidElement, useRef } from 'react';
// import {AriaButtonProps, AriaOverlayProps, Overlay, useModalOverlay} from 'react-aria';
// import { OverlayTriggerProps, OverlayTriggerState } from 'react-stately';
// https://react-spectrum.adobe.com/react-aria/useModalOverlay.html
// Adding a dummy modal with useModalOverlay to see if this solves the problem
// // export const AliasDeletionButton3701 = () => {
// // return (
// // <ModalTrigger label="Open Dialog">
// // {/* {close => */}
// // <Dialog title="Enter your name">
// // <form style={{display: 'flex', flexDirection: 'column'}}>
// // <label htmlFor="first-name">First Name:</label>
// // <input id="first-name" />
// // <label htmlFor="last-name">Last Name:</label>
// // <input id="last-name" />
// // {/* <Button
// // onPress={close}
// // style={{marginTop: 10}}>
// // Submit
// // </Button> */}
// // </form>
// // </Dialog>
// // {/* } */}
// // </ModalTrigger>
// // );
// // }
// type ModalProps = {
// state: OverlayTriggerState;
// children: ReactNode;
// };
// const Modal = (props: ModalProps & AriaOverlayProps) => {
// const ref = useRef(null);
// const { modalProps, underlayProps } = useModalOverlay(props, props.state, ref);
// return (
// <Overlay>
// <div
// style={{
// position: 'fixed',
// zIndex: 100,
// top: 0,
// left: 0,
// bottom: 0,
// right: 0,
// background: 'rgba(0, 0, 0, 0.5)',
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'center'
// }}
// {...underlayProps}
// >
// <p>Testing</p>
// <div
// {...modalProps}
// ref={ref}
// style={{
// background: 'var(--page-background)',
// border: '1px solid gray'
// }}
// >
// {props.children}
// </div>
// </div>
// </Overlay>
// );
// }
// import {useOverlayTrigger} from 'react-aria';
// import {useOverlayTriggerState} from 'react-stately';
// import type {AriaButtonOptions, AriaDialogProps} from 'react-aria';
// import {useDialog} from 'react-aria';
// // Reuse the Button from your component library. See below for details.
// type TriggerProps = {
// label: string;
// // children: (close: () => void) => React.ReactElement<string | React.JSXElementConstructor<ReactNode>>;
// }
// // modal trigger
// export const AliasDeletionButton3701 = (props: TriggerProps & OverlayTriggerProps & AriaOverlayProps) => {
// const state = useOverlayTriggerState(props);
// const { triggerProps, overlayProps } = useOverlayTrigger(
// { type: 'dialog' },
// state
// );
// return (
// <>
// <Button {...triggerProps}>Open Dialog</Button>
// {state.isOpen &&
// (
// <Modal {...props} state={state}>
// {/* {close => */}
// <Dialog title="Enter your name">
// <form style={{display: 'flex', flexDirection: 'column'}}>
// <label htmlFor="first-name">First Name:</label>
// <input id="first-name" />
// <label htmlFor="last-name">Last Name:</label>
// <input id="last-name" />
// <Button
// onPress={state.close}
// style={{marginTop: 10}}>
// Submit
// </Button>
// </form>
// </Dialog>
// {/* } */}
// </Modal>
// )}
// </>
// );
// }
// type DialogProps = {
// title?: React.ReactNode;
// children: React.ReactNode;
// }
// const Dialog = (props: DialogProps & AriaDialogProps) => {
// const ref = useRef(null);
// const { dialogProps, titleProps } = useDialog(props, ref);
// return (
// <div {...dialogProps} ref={ref} style={{ padding: 30 }}>
// {props.title &&
// (
// <h3 {...titleProps} style={{ marginTop: 0 }}>
// {props.title}
// </h3>
// )}
// {props.children}
// </div>
// );
// }
// import {useButton} from 'react-aria';
// type Props = {
// children: ReactNode;
// style: CSSProperties;
// };
// const Button = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement> & AriaButtonOptions<'button'>>(
// (props, buttonRef) => {
// const ref = buttonRef as RefObject<HTMLButtonElement>;
// const { buttonProps } = useButton(props, ref);
// return (
// <button {...buttonProps} ref={ref} style={props.style}>
// {props.children}
// </button>
// );
// }
// );
// Button.displayName = "Button";

Просмотреть файл

@ -35,6 +35,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
.dialog-wrapper {
background: $color-white;

Просмотреть файл

@ -7,9 +7,13 @@ import {
useOverlay,
usePreventScroll,
AriaOverlayProps,
useModalOverlay,
useOverlayTrigger,
Overlay,
PressEvent,
} from "react-aria";
import { useOverlayTriggerState } from "react-stately";
import { FormEventHandler, ReactElement, ReactNode, useRef } from "react";
import { OverlayTriggerState, useOverlayTriggerState } from "react-stately";
import { FormEventHandler, ReactElement, ReactNode, SyntheticEvent, useRef } from "react";
import styles from "./AliasDeletionButtonPermanent.module.scss";
import { Button } from "../../Button";
import { AliasData, getFullAddress } from "../../../hooks/api/aliases";
@ -19,6 +23,7 @@ import { ErrorTriangleIcon } from "../../Icons";
export type Props = {
alias: AliasData;
onDelete: () => void;
modalState: OverlayTriggerState;
};
/**
@ -35,10 +40,15 @@ export const AliasDeletionButtonPermanent = (props: Props) => {
openModalButtonRef,
).buttonProps;
const modalState = useOverlayTriggerState({});
// const modalState = useOverlayTriggerState({});
const modalState = props.modalState;
const { triggerProps, overlayProps } = useOverlayTrigger({
type: 'dialog'
}, modalState);
const cancelButtonRef = useRef<HTMLButtonElement>(null);
const cancelButton = useButton(
{ onPress: () => modalState.close() },
{ onPress: () => { modalState.close() } },
cancelButtonRef,
);
@ -52,11 +62,14 @@ export const AliasDeletionButtonPermanent = (props: Props) => {
};
const dialog = modalState.isOpen ? (
<OverlayContainer>
// <OverlayContainer>
<Overlay>
<ConfirmationDialog
title={l10n.getString("mask-deletion-header")}
onClose={() => modalState.close()}
isOpen={modalState.isOpen}
modalState={modalState}
isDismissable={true}
>
<samp className={styles["alias-to-delete"]}>
@ -74,6 +87,7 @@ export const AliasDeletionButtonPermanent = (props: Props) => {
{...cancelButton.buttonProps}
ref={cancelButtonRef}
className={styles["cancel-button"]}
onMouseDown={(e) => {e.stopPropagation(); console.log('clicked');}}
>
{l10n.getString("profile-label-cancel")}
</button>
@ -87,13 +101,15 @@ export const AliasDeletionButtonPermanent = (props: Props) => {
</div>
</form>
</ConfirmationDialog>
</OverlayContainer>
{/* </OverlayContainer> */}
</Overlay>
) : null;
return (
<>
<button
{...openModalButtonProps}
{...triggerProps}
className={styles["deletion-button"]}
ref={openModalButtonRef}
>
@ -121,15 +137,17 @@ type ConfirmationDialogProps = {
title: string | ReactElement;
children: ReactNode;
isOpen: boolean;
modalState: OverlayTriggerState;
onClose?: () => void;
};
const ConfirmationDialog = (
props: ConfirmationDialogProps & AriaOverlayProps,
) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const { overlayProps, underlayProps } = useOverlay(props, wrapperRef);
usePreventScroll();
const { modalProps } = useModal();
// const { overlayProps, underlayProps } = useOverlay(props, wrapperRef);
const { modalProps, underlayProps } = useModalOverlay(props, props.modalState, wrapperRef);
// usePreventScroll();
// const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog({}, wrapperRef);
return (
@ -137,7 +155,7 @@ const ConfirmationDialog = (
<FocusScope contain restoreFocus autoFocus>
<div
className={styles["dialog-wrapper"]}
{...overlayProps}
// {...overlayProps}
{...dialogProps}
{...modalProps}
ref={wrapperRef}

Просмотреть файл

@ -16,6 +16,7 @@ import { Localized } from "../../Localized";
import { VisuallyHidden } from "../../VisuallyHidden";
import { MaskCard } from "./MaskCard";
import { isFlagActive } from "../../../functions/waffle";
import { OverlayTriggerState } from "react-stately";
export type Props = {
aliases: AliasData[];
@ -31,6 +32,7 @@ export type Props = {
onDelete: (alias: AliasData) => void;
onboarding?: boolean;
children?: React.ReactNode;
modalState: OverlayTriggerState;
};
/**
@ -162,6 +164,7 @@ export const AliasList = (props: Props) => {
runtimeData={props.runtimeData}
isOnboarding={onboarding}
copyAfterMaskGeneration={generatedAlias?.id === alias.id}
modalState={props.modalState}
>
{props.children}
</MaskCard>

Просмотреть файл

@ -10,6 +10,7 @@ import {
useState,
} from "react";
import {
OverlayTriggerState,
RadioGroupProps,
RadioGroupState,
useRadioGroupState,
@ -45,6 +46,7 @@ import { AliasDeletionButton } from "./AliasDeletionButton";
import { VisuallyHidden } from "./../../VisuallyHidden";
import HorizontalArrow from "./../images/free-onboarding-horizontal-arrow.svg";
import { AliasDeletionButtonPermanent } from "./AliasDeletionButtonPermanent";
// import { AliasDeletionButton3701 } from "./AliasDeletionButton3701";
export type Props = {
mask: AliasData;
@ -60,6 +62,7 @@ export type Props = {
isOnboarding?: boolean;
children?: ReactNode;
copyAfterMaskGeneration: boolean;
modalState: OverlayTriggerState;
};
export const MaskCard = (props: Props) => {
@ -388,7 +391,9 @@ export const MaskCard = (props: Props) => {
props.runtimeData,
"custom_domain_management_redesign",
) ? (
// <AliasDeletionButton3701 isDismissable={true} label="Open Dialog"/>
<AliasDeletionButtonPermanent
modalState={props.modalState}
onDelete={props.onDelete}
alias={props.mask}
/>

Просмотреть файл

@ -1,6 +1,10 @@
@import "../../styles/tokens.scss";
@import "~@mozilla-protocol/core/protocol/css/includes/lib";
.patch3701 {
pointer-events: none;
}
.wrapper {
display: flex;
flex-direction: column;

Просмотреть файл

@ -32,12 +32,14 @@ import { isPhonesAvailableInCountry } from "../../functions/getPlan";
import { useL10n } from "../../hooks/l10n";
import { HolidayPromoBanner } from "./topmessage/HolidayPromoBanner";
import { isFlagActive } from "../../functions/waffle";
import { OverlayTriggerState } from "react-stately";
export type Props = {
children: ReactNode;
// Plain page used for pages without the typical header bag, e.g. tracker report page
theme?: "free" | "premium" | "plain";
runtimeData?: RuntimeData;
modalState?: OverlayTriggerState;
};
/**
@ -130,10 +132,26 @@ export const Layout = (props: Props) => {
</div>
);
const [pointerEventsNone, setPointerEventsNone] = useState(false)
useEffect(() => {
let modalState = props.modalState;
// We want to temporarily disable pointer events as soon as the modal closes.
if (!modalState?.isOpen) {
const id = setTimeout(() => {
setPointerEventsNone(false);
}, 0);
return;
}
setPointerEventsNone(true)
}, [props.modalState?.isOpen, pointerEventsNone])
return (
<>
<PageMetadata />
<div className={styles.wrapper}>
<div className={`${pointerEventsNone && styles.patch3701} ${styles.wrapper}`}>
{apiMockWarning}
<TopMessage
profile={profiles.data?.[0]}

Просмотреть файл

@ -6,6 +6,7 @@ import {
ReactNode,
RefObject,
useRef,
useState,
} from "react";
import {
FocusScope,
@ -18,7 +19,7 @@ import {
useTooltipTrigger,
} from "react-aria";
import { event as gaEvent } from "react-ga";
import { useMenuTriggerState, useTooltipTriggerState } from "react-stately";
import { useMenuTriggerState, useOverlayTriggerState, useTooltipTriggerState } from "react-stately";
import { toast } from "react-toastify";
import styles from "./profile.module.scss";
import UpsellBannerUs from "./images/upsell-banner-us.svg";
@ -81,6 +82,7 @@ const Profile: NextPage = () => {
clearCookie("profile-location-hash");
}
usePurchaseTracker(profileData.data?.[0]);
const modalState = useOverlayTriggerState({});
if (!userData.isValidating && userData.error) {
if (document.location.hash) {
@ -564,7 +566,7 @@ const Profile: NextPage = () => {
}}
/>
)}
<Layout runtimeData={runtimeData.data}>
<Layout modalState={modalState} runtimeData={runtimeData.data}>
{/* If free user has reached their free mask limit and
premium is available in their country, show upsell banner */}
{freeMaskLimitReached &&
@ -590,6 +592,7 @@ const Profile: NextPage = () => {
profile={profile}
user={user}
runtimeData={runtimeData.data}
modalState={modalState}
/>
<p className={styles["size-information"]}>
{l10n.getString("profile-supports-email-forwarding", {