diff --git a/packages/utils/src/Domain/Utils/ClassNames.ts b/packages/utils/src/Domain/Utils/ClassNames.ts index bea157f5d..e641d96fa 100644 --- a/packages/utils/src/Domain/Utils/ClassNames.ts +++ b/packages/utils/src/Domain/Utils/ClassNames.ts @@ -1,3 +1,3 @@ -export const classNames = (...values: (string | boolean | undefined)[]): string => { +export const classNames = (...values: any[]): string => { return values.map((value) => (typeof value === 'string' ? value : null)).join(' ') } diff --git a/packages/web/src/javascripts/Components/CameraCaptureModal/PhotoCaptureModal.tsx b/packages/web/src/javascripts/Components/CameraCaptureModal/PhotoCaptureModal.tsx index cb64a3f48..74727a017 100644 --- a/packages/web/src/javascripts/Components/CameraCaptureModal/PhotoCaptureModal.tsx +++ b/packages/web/src/javascripts/Components/CameraCaptureModal/PhotoCaptureModal.tsx @@ -4,14 +4,10 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils' import { classNames } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Button from '../Button/Button' import Dropdown from '../Dropdown/Dropdown' import Icon from '../Icon/Icon' import DecoratedInput from '../Input/DecoratedInput' -import ModalDialog from '../Shared/ModalDialog' -import ModalDialogButtons from '../Shared/ModalDialogButtons' -import ModalDialogDescription from '../Shared/ModalDialogDescription' -import ModalDialogLabel from '../Shared/ModalDialogLabel' +import Modal from '../Shared/Modal' type Props = { filesController: FilesController @@ -87,10 +83,43 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => { close() }, [capturedPhoto, close, fileName, filesController]) + const retryPhoto = () => { + setCapturedPhoto(undefined) + setRecorder(new PhotoRecorder()) + } return ( - - Take a photo - + +
)} - - - {!capturedPhoto && ( - - )} - {capturedPhoto && ( -
- - -
- )} -
- +
+
) } diff --git a/packages/web/src/javascripts/Components/CameraCaptureModal/VideoCaptureModal.tsx b/packages/web/src/javascripts/Components/CameraCaptureModal/VideoCaptureModal.tsx index c847fc011..45fb23795 100644 --- a/packages/web/src/javascripts/Components/CameraCaptureModal/VideoCaptureModal.tsx +++ b/packages/web/src/javascripts/Components/CameraCaptureModal/VideoCaptureModal.tsx @@ -4,13 +4,9 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils' import { classNames } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Button from '../Button/Button' import Icon from '../Icon/Icon' import DecoratedInput from '../Input/DecoratedInput' -import ModalDialog from '../Shared/ModalDialog' -import ModalDialogButtons from '../Shared/ModalDialogButtons' -import ModalDialogDescription from '../Shared/ModalDialogDescription' -import ModalDialogLabel from '../Shared/ModalDialogLabel' +import Modal from '../Shared/Modal' type Props = { filesController: FilesController @@ -77,10 +73,59 @@ const VideoCaptureModal = ({ filesController, close }: Props) => { return URL.createObjectURL(capturedVideo) }, [capturedVideo]) + const stopRecording = async () => { + const capturedVideo = await recorder.stop() + setIsRecording(false) + setCapturedVideo(capturedVideo) + } + + const retryRecording = () => { + setCapturedVideo(undefined) + setRecorder(new VideoRecorder(fileName)) + setIsRecorderReady(false) + } + return ( - - Record a video - + +
)}
-
- - {!capturedVideo && !isRecording && ( - - )} - {!capturedVideo && isRecording && ( - - )} - {capturedVideo && ( -
- - -
- )} -
-
+ + ) } diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx index 89a81d031..029d34fce 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -19,8 +19,10 @@ import { ApplicationGroup } from '@/Application/ApplicationGroup' import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { ChallengeModalValues } from './ChallengeModalValues' import { InputValue } from './InputValue' -import { isMobileScreen } from '@/Utils' +import { isIOS, isMobileScreen } from '@/Utils' import { classNames } from '@standardnotes/utils' +import MobileModalAction from '../Shared/MobileModalAction' +import { useModalAnimation } from '../Shared/useModalAnimation' type Props = { application: WebApplication @@ -209,98 +211,121 @@ const ChallengeModal: FunctionComponent = ({ } }, [application, cancelChallenge, challenge.cancelable]) - if (!challenge.prompts) { + const [isMounted, setElement] = useModalAnimation(!!challenge.prompts.length) + + if (!isMounted) { return null } const isFullScreenBlocker = challenge.reason === ChallengeReason.ApplicationUnlock const isMobileOverlay = isMobileScreen() && !isFullScreenBlocker - const contentClasses = classNames( - 'challenge-modal relative flex flex-col items-center rounded border-solid border-border p-8 md:border', - !isMobileScreen() && 'shadow-overlay-light', - isMobileOverlay && 'border border-solid border-border shadow-overlay-light', - isFullScreenBlocker && isMobileScreen() ? 'bg-passive-5' : 'bg-default', - ) - return ( - + +
+
+ {challenge.cancelable ? ( + + Cancel + + ) : ( +
+ )} +
Authenticate
+
+
+
{challenge.cancelable && ( )} - -
{challenge.heading}
- {challenge.subheading && ( -
{challenge.subheading}
- )} -
{ - e.preventDefault() - submit() - }} - ref={promptsContainerRef} - > - {challenge.prompts.map((prompt, index) => ( - - ))} - - - {shouldShowForgotPasscode && ( - - )} - {shouldShowWorkspaceSwitcher && ( - - )} + {shouldShowForgotPasscode && ( + + )} + {shouldShowWorkspaceSwitcher && ( + + )} +
) diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx index 62c3fd564..e422a3dfb 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx @@ -99,14 +99,16 @@ const ChallengeModalPrompt: FunctionComponent = ({ return (
-
- - - - - - - - - - { - setShowOptionsMenu(false) - }} - side="bottom" - align="start" - className="py-2" - overrideZIndex="z-modal" - > - - { - setShowOptionsMenu(false) +
+
+
+
{IconComponent}
+ {isRenaming ? ( + { + if (event.key === KeyboardKey.Enter) { + void handleRename() + } }} - shouldShowRenameOption={false} - shouldShowAttachOption={false} + right={[ + , + ]} + ref={mergeRefs([renameInputRef, focusInputOnMount])} /> -
-
- -
+
+ + + + + + + + + + { + setShowOptionsMenu(false) + }} + side="bottom" + align="start" + className="py-2" + overrideZIndex="z-modal" > - + + { + setShowOptionsMenu(false) + }} + shouldShowRenameOption={false} + shouldShowAttachOption={false} + /> + + + + + + - - +
+ + {showLinkedBubblesContainer && ( +
+ +
+ )} +
+
+ +
+ {showFileInfoPanel && }
- {showLinkedBubblesContainer && ( -
- -
- )} -
-
- -
- {showFileInfoPanel && } -
- -
-
- ) -}) + + + ) + }), +) FilePreviewModal.displayName = 'FilePreviewModal' const FilePreviewModalWrapper: FunctionComponent = ({ application, viewControllerManager }) => { - return viewControllerManager.filePreviewModalController.isOpen ? ( - - ) : null + const [isMounted, setElement] = useModalAnimation(viewControllerManager.filePreviewModalController.isOpen) + + if (!isMounted) { + return null + } + + return } export default observer(FilePreviewModalWrapper) diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx index d94a02d8b..bb19c48d7 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx @@ -2,17 +2,14 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { ContentType, DecryptedTransferPayload, pluralize, SNTag, TagContent, UuidGenerator } from '@standardnotes/snjs' import { Importer } from '@standardnotes/ui-services' import { observer } from 'mobx-react-lite' -import { useCallback, useReducer, useState } from 'react' +import { useCallback, useMemo, useReducer, useState } from 'react' import { useApplication } from '../ApplicationProvider' -import Button from '../Button/Button' import { useStateRef } from '@/Hooks/useStateRef' -import ModalDialog from '../Shared/ModalDialog' -import ModalDialogButtons from '../Shared/ModalDialogButtons' -import ModalDialogDescription from '../Shared/ModalDialogDescription' -import ModalDialogLabel from '../Shared/ModalDialogLabel' import { ImportModalFileItem } from './ImportModalFileItem' import ImportModalInitialPage from './InitialPage' import { ImportModalAction, ImportModalFile, ImportModalState } from './Types' +import Modal, { ModalAction } from '../Shared/Modal' +import ModalOverlay from '../Shared/ModalOverlay' const reducer = (state: ImportModalState, action: ImportModalAction): ImportModalState => { switch (action.type) { @@ -190,36 +187,44 @@ const ImportModal = ({ viewControllerManager }: { viewControllerManager: ViewCon }) }, [state.importTag, viewControllerManager.isImportModalVisible, viewControllerManager.navigationController]) - if (!viewControllerManager.isImportModalVisible.get()) { - return null - } + const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready') + const importSuccessOrError = + files.length > 0 && files.every((file) => file.status === 'success' || file.status === 'error') + + const modalActions: ModalAction[] = useMemo( + () => [ + { + label: 'Import', + type: 'primary', + onClick: parseAndImport, + hidden: !isReadyToImport, + mobileSlot: 'right', + }, + { + label: importSuccessOrError ? 'Close' : 'Cancel', + type: 'cancel', + onClick: closeDialog, + mobileSlot: 'left', + }, + ], + [closeDialog, importSuccessOrError, isReadyToImport, parseAndImport], + ) return ( - - Import - - {!files.length && } - {files.length > 0 && ( -
- {files.map((file) => ( - - ))} -
- )} -
- - {files.length > 0 && files.every((file) => file.status === 'ready') && ( - - )} - - -
+ + +
+ {!files.length && } + {files.length > 0 && ( +
+ {files.map((file) => ( + + ))} +
+ )} +
+
+
) } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx index a29eef32c..d31035b5f 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -40,6 +40,7 @@ import { getPlaintextFontSize } from '@/Utils/getPlaintextFontSize' import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin' import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context' import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin' +import ModalOverlay from '@/Components/Shared/ModalOverlay' const NotePreviewCharLimit = 160 @@ -205,7 +206,9 @@ export const SuperEditor: FunctionComponent = ({ - {showMarkdownPreview && } + + + ) diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx index 3605aaacc..af02e13e2 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx @@ -1,16 +1,12 @@ import { WebApplication } from '@/Application/Application' import { NoteType, SNNote } from '@standardnotes/snjs' -import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor' import { ErrorBoundary } from '@/Utils/ErrorBoundary' -import ModalDialog from '@/Components/Shared/ModalDialog' -import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons' -import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' -import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' -import Button from '@/Components/Button/Button' import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin' import { NoteViewController } from '../Controller/NoteViewController' import { spaceSeparatedStrings } from '@standardnotes/utils' +import Modal, { ModalAction } from '@/Components/Shared/Modal' const NotePreviewCharLimit = 160 @@ -82,51 +78,55 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, onConvertComplete() }, [closeDialog, application, note, onConvertComplete, performConvert]) + const modalActions: ModalAction[] = useMemo( + () => [ + { + label: 'Cancel', + onClick: closeDialog, + type: 'cancel', + mobileSlot: 'left', + }, + { + label: 'Convert', + onClick: confirmConvert, + mobileSlot: 'right', + type: 'primary', + }, + { + label: 'Convert As-Is', + onClick: convertAsIs, + type: 'secondary', + }, + ], + [closeDialog, confirmConvert, convertAsIs], + ) + if (isSeamlessConvert) { return null } return ( - - - Convert to Super note -

- The following is a preview of how your note will look when converted to Super. Super notes use a custom format - under the hood. Converting your note will transition it from plaintext to the custom Super format. -

-
- -
- - - - - - - -
-
- -
-
- -
-
- -
- -
-
- - + +
+ The following is a preview of how your note will look when converted to Super. Super notes use a custom format + under the hood. Converting your note will transition it from plaintext to the custom Super format. +
+
+ + + + + + + +
+
) } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteMarkdownPreview.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteMarkdownPreview.tsx index 75c691fcd..006156b88 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteMarkdownPreview.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteMarkdownPreview.tsx @@ -1,16 +1,12 @@ import { SNNote } from '@standardnotes/snjs' -import { FunctionComponent, useCallback, useState } from 'react' +import { FunctionComponent, useCallback, useMemo, useState } from 'react' import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor' import { ErrorBoundary } from '@/Utils/ErrorBoundary' -import ModalDialog from '@/Components/Shared/ModalDialog' -import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons' -import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' -import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' -import Button from '@/Components/Button/Button' import MarkdownPreviewPlugin from './Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin' import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode' import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode' import { copyTextToClipboard } from '../../../Utils/copyTextToClipboard' +import Modal, { ModalAction } from '@/Components/Shared/Modal' type Props = { note: SNNote @@ -33,33 +29,33 @@ export const SuperNoteMarkdownPreview: FunctionComponent = ({ note, close setMarkdown(markdown) }, []) + const modalActions: ModalAction[] = useMemo( + () => [ + { + label: didCopy ? 'Copied' : 'Copy', + type: 'primary', + onClick: copy, + mobileSlot: 'left', + }, + ], + [copy, didCopy], + ) + return ( - - Markdown Preview - -
- - - - - - - -
-
- -
- -
- -
- - + +
+ + + + + + + +
+
) } diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx index e92ef7528..5cf11b59f 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx @@ -1,12 +1,10 @@ import { WebApplication } from '@/Application/Application' import { createRef } from 'react' import { AbstractComponent } from '@/Components/Abstract/PureComponent' -import Button from '@/Components/Button/Button' import DecoratedPasswordInput from '../Input/DecoratedPasswordInput' -import ModalDialog from '../Shared/ModalDialog' -import ModalDialogLabel from '../Shared/ModalDialogLabel' -import ModalDialogDescription from '../Shared/ModalDialogDescription' -import ModalDialogButtons from '../Shared/ModalDialogButtons' +import Modal from '../Shared/Modal' +import { isMobileScreen } from '@/Utils' +import Spinner from '../Spinner/Spinner' interface Props { application: WebApplication @@ -25,6 +23,8 @@ type State = { } const DEFAULT_CONTINUE_TITLE = 'Continue' +const GENERATING_CONTINUE_TITLE = 'Generating Keys...' +const FINISH_CONTINUE_TITLE = 'Finish' enum Steps { PasswordStep = 1, @@ -89,7 +89,7 @@ class PasswordWizard extends AbstractComponent { this.setState({ isContinuing: true, showSpinner: true, - continueTitle: 'Generating Keys...', + continueTitle: GENERATING_CONTINUE_TITLE, }) const valid = await this.validateCurrentPassword() @@ -107,7 +107,7 @@ class PasswordWizard extends AbstractComponent { this.setState({ isContinuing: false, showSpinner: false, - continueTitle: 'Finish', + continueTitle: FINISH_CONTINUE_TITLE, step: Steps.FinishStep, }) } @@ -228,10 +228,32 @@ class PasswordWizard extends AbstractComponent { override render() { return ( -
- - {this.state.title} - +
+ + ) : ( + this.state.continueTitle + ), + onClick: this.nextStep, + type: 'primary', + mobileSlot: 'right', + disabled: this.state.lockContinue, + }, + ]} + > +
{this.state.step === Steps.PasswordStep && (
@@ -284,13 +306,8 @@ class PasswordWizard extends AbstractComponent {

)} - - - - - +
+
) } diff --git a/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModal.tsx b/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModal.tsx index fccd41dd7..b2c5c391e 100644 --- a/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModal.tsx +++ b/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModal.tsx @@ -1,10 +1,8 @@ import { SNComponent } from '@standardnotes/snjs' import { useCallback } from 'react' import Button from '@/Components/Button/Button' -import ModalDialog from '../Shared/ModalDialog' -import ModalDialogLabel from '../Shared/ModalDialogLabel' -import ModalDialogDescription from '../Shared/ModalDialogDescription' import ModalDialogButtons from '../Shared/ModalDialogButtons' +import Modal from '../Shared/Modal' type Props = { callback: (approved: boolean) => void @@ -25,9 +23,30 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P }, [callback, dismiss]) return ( - - Activate Component - + + + + +
+ } + > +
{component.displayName} {' would like to interact with your '} @@ -41,13 +60,8 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P

- - - - - +
+ ) } diff --git a/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx b/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx index 37412b7db..da64f9614 100644 --- a/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx +++ b/packages/web/src/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx @@ -1,6 +1,7 @@ import { WebApplication } from '@/Application/Application' import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs' import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import ModalOverlay from '../Shared/ModalOverlay' import PermissionsModal from './PermissionsModal' type Props = { @@ -42,14 +43,18 @@ const PermissionsModalWrapper: FunctionComponent = ({ application }) => { } }, [application, onAppStart]) - return dialog ? ( - - ) : null + return ( + + {dialog && ( + + )} + + ) } export default PermissionsModalWrapper diff --git a/packages/web/src/javascripts/Components/Popover/MobilePopoverContent.tsx b/packages/web/src/javascripts/Components/Popover/MobilePopoverContent.tsx index 1986261cd..c536d53bc 100644 --- a/packages/web/src/javascripts/Components/Popover/MobilePopoverContent.tsx +++ b/packages/web/src/javascripts/Components/Popover/MobilePopoverContent.tsx @@ -1,8 +1,14 @@ import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile' -import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation' import { classNames } from '@standardnotes/snjs' import { ReactNode } from 'react' import Portal from '../Portal/Portal' +import { useModalAnimation } from '../Shared/useModalAnimation' + +const DisableScroll = () => { + useDisableBodyScrollOnMobile() + + return null +} const MobilePopoverContent = ({ open, @@ -17,54 +23,7 @@ const MobilePopoverContent = ({ title: string className?: string }) => { - const [isMounted, setPopoverElement] = useLifecycleAnimation({ - open, - enter: { - keyframes: [ - { - opacity: 0.25, - transform: 'translateY(1rem)', - }, - { - opacity: 1, - transform: 'translateY(0)', - }, - ], - options: { - easing: 'cubic-bezier(.36,.66,.04,1)', - duration: 150, - fill: 'forwards', - }, - initialStyle: { - transformOrigin: 'bottom', - }, - }, - enterCallback: (element) => { - element.scrollTop = 0 - }, - exit: { - keyframes: [ - { - opacity: 1, - transform: 'translateY(0)', - }, - { - opacity: 0, - transform: 'translateY(1rem)', - }, - ], - options: { - easing: 'cubic-bezier(.36,.66,.04,1)', - duration: 150, - fill: 'forwards', - }, - initialStyle: { - transformOrigin: 'bottom', - }, - }, - }) - - useDisableBodyScrollOnMobile() + const [isMounted, setPopoverElement] = useModalAnimation(open) if (!isMounted) { return null @@ -72,14 +31,15 @@ const MobilePopoverContent = ({ return ( +
{title}
-
diff --git a/packages/web/src/javascripts/Components/Popover/Popover.tsx b/packages/web/src/javascripts/Components/Popover/Popover.tsx index a8cd4dec7..6f4ab1fbc 100644 --- a/packages/web/src/javascripts/Components/Popover/Popover.tsx +++ b/packages/web/src/javascripts/Components/Popover/Popover.tsx @@ -45,6 +45,7 @@ const Popover = ({ disableClickOutside, disableMobileFullscreenTakeover, maxHeight, + portal, }: Props) => { const popoverId = useRef(UuidGenerator.GenerateUuid()) @@ -123,6 +124,7 @@ const Popover = ({ side={side} title={title} togglePopover={togglePopover} + portal={portal} > {children} diff --git a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx index f6abd927d..828c6d23b 100644 --- a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx +++ b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx @@ -2,9 +2,7 @@ import { useDocumentRect } from '@/Hooks/useDocumentRect' import { useAutoElementRect } from '@/Hooks/useElementRect' import { classNames } from '@standardnotes/utils' import { useCallback, useLayoutEffect, useState } from 'react' -import Icon from '../Icon/Icon' import Portal from '../Portal/Portal' -import HorizontalSeparator from '../Shared/HorizontalSeparator' import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles' import { PopoverContentProps } from './Types' import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside' @@ -25,6 +23,7 @@ const PositionedPopoverContent = ({ disableClickOutside, disableMobileFullscreenTakeover, maxHeight, + portal = true, }: PopoverContentProps) => { const [popoverElement, setPopoverElement] = useState(null) const popoverRect = useAutoElementRect(popoverElement) @@ -72,7 +71,7 @@ const PositionedPopoverContent = ({ }, [popoverElement, correctInitialScrollForOverflowedContent]) return ( - +
-
-
- -
- -
-
{children}
+ {children}
) diff --git a/packages/web/src/javascripts/Components/Popover/Types.ts b/packages/web/src/javascripts/Components/Popover/Types.ts index 25d2870fb..f38174acc 100644 --- a/packages/web/src/javascripts/Components/Popover/Types.ts +++ b/packages/web/src/javascripts/Components/Popover/Types.ts @@ -30,16 +30,6 @@ type PopoverAnchorPointProps = { anchorElement?: never } -type PopoverMutuallyExclusiveProps = - | { - togglePopover: () => void - disableMobileFullscreenTakeover?: never - } - | { - togglePopover?: never - disableMobileFullscreenTakeover: boolean - } - type CommonPopoverProps = { align?: PopoverAlignment children: ReactNode @@ -48,7 +38,10 @@ type CommonPopoverProps = { className?: string disableClickOutside?: boolean maxHeight?: (calculatedMaxHeight: number) => number + togglePopover?: () => void + disableMobileFullscreenTakeover?: boolean title: string + portal?: boolean } export type PopoverContentProps = CommonPopoverProps & { @@ -61,5 +54,5 @@ export type PopoverContentProps = CommonPopoverProps & { } export type PopoverProps = - | (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorElementProps) - | (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorPointProps) + | (CommonPopoverProps & PopoverAnchorElementProps) + | (CommonPopoverProps & PopoverAnchorPointProps) diff --git a/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts b/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts index 7a0d676d6..e27bb1e99 100644 --- a/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts +++ b/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts @@ -1,4 +1,3 @@ -import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery' import { useEffect } from 'react' type Options = { @@ -18,19 +17,14 @@ export const usePopoverCloseOnClickOutside = ({ }: Options) => { useEffect(() => { const closeIfClickedOutside = (event: MouseEvent) => { - const matchesMediumBreakpoint = matchMedia(MediaQueryBreakpoints.md).matches - - if (!matchesMediumBreakpoint) { - return - } - const target = event.target as Element const isDescendantOfMenu = popoverElement?.contains(target) const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false const closestPopoverId = target.closest('[data-popover]')?.getAttribute('data-popover') const isDescendantOfChildPopover = closestPopoverId && childPopovers.has(closestPopoverId) - const isDescendantOfModal = !!target.closest('[aria-modal="true"]') + const isPopoverInModal = popoverElement?.closest('[aria-modal="true"]') + const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[aria-modal="true"]') if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover && !isDescendantOfModal) { if (!disabled) { diff --git a/packages/web/src/javascripts/Components/Portal/Portal.tsx b/packages/web/src/javascripts/Components/Portal/Portal.tsx index bbefc1fb3..37d950f18 100644 --- a/packages/web/src/javascripts/Components/Portal/Portal.tsx +++ b/packages/web/src/javascripts/Components/Portal/Portal.tsx @@ -3,11 +3,12 @@ import { createPortal } from 'react-dom' type Props = { children: ReactNode + disabled?: boolean } const randomPortalId = () => Math.random() -const Portal = ({ children }: Props) => { +const Portal = ({ children, disabled = false }: Props) => { const [container, setContainer] = useState() useEffect(() => { @@ -18,6 +19,10 @@ const Portal = ({ children }: Props) => { return () => container.remove() }, []) + if (disabled) { + return <>{children} + } + return container ? createPortal(children, container) : null } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx index 06d982ba0..7b8b368a1 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx @@ -1,13 +1,9 @@ -import ModalDialog from '@/Components/Shared/ModalDialog' -import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons' -import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' -import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' -import Button from '@/Components/Button/Button' -import { FunctionComponent, useState } from 'react' +import { FunctionComponent, useCallback, useMemo, useState } from 'react' import { WebApplication } from '@/Application/Application' import { useBeforeUnload } from '@/Hooks/useBeforeUnload' import ChangeEmailForm from './ChangeEmailForm' import ChangeEmailSuccess from './ChangeEmailSuccess' +import Modal, { ModalAction } from '@/Components/Shared/Modal' enum SubmitButtonTitles { Default = 'Continue', @@ -37,7 +33,7 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = const applicationAlertService = application.alertService - const validateCurrentPassword = async () => { + const validateCurrentPassword = useCallback(async () => { if (!currentPassword || currentPassword.length === 0) { applicationAlertService.alert('Please enter your current password.').catch(console.error) @@ -54,14 +50,14 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = } return success - } + }, [application, applicationAlertService, currentPassword]) const resetProgressState = () => { setSubmitButtonTitle(SubmitButtonTitles.Default) setIsContinuing(false) } - const processEmailChange = async () => { + const processEmailChange = useCallback(async () => { await application.downloadBackup() setLockContinue(true) @@ -73,17 +69,17 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = setLockContinue(false) return success - } + }, [application, currentPassword, newEmail]) - const dismiss = () => { + const dismiss = useCallback(() => { if (lockContinue) { applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) } else { onCloseDialog() } - } + }, [applicationAlertService, lockContinue, onCloseDialog]) - const handleSubmit = async () => { + const handleSubmit = useCallback(async () => { if (lockContinue || isContinuing) { return } @@ -115,31 +111,43 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = setIsContinuing(false) setSubmitButtonTitle(SubmitButtonTitles.Finish) setCurrentStep(Steps.FinishStep) - } + }, [currentStep, dismiss, isContinuing, lockContinue, processEmailChange, validateCurrentPassword]) - const handleDialogClose = () => { + const handleDialogClose = useCallback(() => { if (lockContinue) { applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) } else { onCloseDialog() } - } + }, [applicationAlertService, lockContinue, onCloseDialog]) + + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: 'Cancel', + onClick: handleDialogClose, + type: 'cancel', + mobileSlot: 'left', + }, + { + label: submitButtonTitle, + onClick: handleSubmit, + type: 'primary', + mobileSlot: 'right', + }, + ], + [handleDialogClose, handleSubmit, submitButtonTitle], + ) return ( -
- - Change Email - - {currentStep === Steps.InitialStep && ( - - )} - {currentStep === Steps.FinishStep && } - - -
+ +
+ {currentStep === Steps.InitialStep && ( + + )} + {currentStep === Steps.FinishStep && } +
+
) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx index a3f7a9322..7170aea70 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx @@ -10,6 +10,7 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import ModalOverlay from '@/Components/Shared/ModalOverlay' type Props = { application: WebApplication @@ -33,6 +34,8 @@ const Credentials: FunctionComponent = ({ application }: Props) => { setShouldShowPasswordWizard(false) }, []) + const closeChangeEmailDialog = () => setIsChangeEmailDialogOpen(false) + return ( <> @@ -55,14 +58,14 @@ const Credentials: FunctionComponent = ({ application }: Props) => { Current password was set on {passwordCreatedOn}
+ +
+ {currentStep === Steps.InitialStep && } + {currentStep === Steps.FinishStep && } +
+
) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx index 77bb788a8..2088118d7 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx @@ -14,6 +14,7 @@ import InvitationsList from './InvitationsList' import Invite from './Invite/Invite' import Button from '@/Components/Button/Button' import SharingStatusText from './SharingStatusText' +import ModalOverlay from '@/Components/Shared/ModalOverlay' type Props = { application: WebApplication @@ -28,6 +29,8 @@ const SubscriptionSharing: FunctionComponent = ({ application, viewContro const isSubscriptionSharingFeatureAvailable = application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled + const closeInviteDialog = () => setIsInviteDialogOpen(false) + return ( @@ -42,13 +45,13 @@ const SubscriptionSharing: FunctionComponent = ({ application, viewContro {!subscriptionState.allInvitationsUsed && (
) : ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModal.tsx index 821db3e73..2a00792ee 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModal.tsx @@ -1,15 +1,11 @@ -import Button from '@/Components/Button/Button' import Icon from '@/Components/Icon/Icon' import IconPicker from '@/Components/Icon/IconPicker' import Popover from '@/Components/Popover/Popover' -import ModalDialog from '@/Components/Shared/ModalDialog' -import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons' -import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' -import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' +import Modal, { ModalAction } from '@/Components/Shared/Modal' import Spinner from '@/Components/Spinner/Spinner' import { Platform, SmartViewDefaultIconName, VectorIconNameOrEmoji } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { EditSmartViewModalController } from './EditSmartViewModalController' type Props = { @@ -63,15 +59,40 @@ const EditSmartViewModal = ({ controller, platform }: Props) => { } }, [isPredicateJsonValid]) + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: 'Delete', + onClick: deleteView, + disabled: isSaving, + type: 'destructive', + }, + { + label: 'Cancel', + onClick: closeDialog, + disabled: isSaving, + type: 'cancel', + mobileSlot: 'left', + }, + { + label: isSaving ? : 'Save', + onClick: saveSmartView, + disabled: isSaving, + type: 'primary', + mobileSlot: 'right', + }, + ], + [closeDialog, deleteView, isSaving, saveSmartView], + ) + if (!view) { return null } return ( - - Edit Smart View "{view.title}" - -
+ +
+
Title:
{
-
+
Predicate:
-
+