diff --git a/panda.config.ts b/panda.config.ts index aaf51d9680..fcdfea3d28 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -285,12 +285,8 @@ export default defineConfig({ }, }, fontSizes: { - sm: { - value: '80%', - }, - md: { - value: '90%', - }, + sm: { value: '80%' }, + md: { value: '90%' }, }, spacing: { modalPadding: { value: '8%' }, diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx index d61747e8bc..5bce4313c0 100644 --- a/src/components/Alert.tsx +++ b/src/components/Alert.tsx @@ -1,12 +1,15 @@ -import React, { FC, useRef, useState } from 'react' +import React, { FC, useCallback, useRef, useState } from 'react' import { useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { TransitionGroup } from 'react-transition-group' import { css } from '../../styled-system/css' import { token } from '../../styled-system/tokens' +import { alertActionCreator } from '../actions/alert' import { AlertType } from '../constants' import alertStore from '../stores/alert' import strip from '../util/strip' import FadeTransition from './FadeTransition' +import PopupBase from './PopupBase' import RedoIcon from './RedoIcon' import UndoIcon from './UndoIcon' @@ -24,6 +27,14 @@ const Alert: FC = () => { const value = strip(alertStoreValue ?? alert?.value ?? '') const fontSize = useSelector(state => state.fontSize) const iconSize = 0.78 * fontSize + const dispatch = useDispatch() + + /** Dismiss the alert on close. */ + const onClose = useCallback(() => { + if (!alert?.showCloseLink) return + setDismiss(true) + dispatch(alertActionCreator(null)) + }, [alert, dispatch]) const Icon = alert?.alertType && alert.alertType in alertToIcon ? alertToIcon[alert.alertType as keyof typeof alertToIcon] : null @@ -38,7 +49,8 @@ const Alert: FC = () => { {alert ? ( setDismiss(false)}> {/* Specify a key to force the component to re-render and thus recalculate useSwipeToDismissProps when the alert changes. Otherwise the alert gets stuck off screen in the dismiss state. */} -
{ ref={popupRef} key={value} data-testid='alert-content' + onClose={onClose} + closeButtonSize='sm' > {renderedIcon} {value} -
+
) : null} diff --git a/src/components/CloseButton.tsx b/src/components/CloseButton.tsx index d3381de182..cfd15298cf 100644 --- a/src/components/CloseButton.tsx +++ b/src/components/CloseButton.tsx @@ -4,23 +4,28 @@ import { upperRightRecipe } from '../../styled-system/recipes' import fastClick from '../util/fastClick' /** A close button with an ✕. */ -const CloseButton = ({ onClose, disableSwipeToDismiss }: { onClose: () => void; disableSwipeToDismiss?: boolean }) => { +const CloseButton = ({ + onClose, + disableSwipeToDismiss, + size = 'md', +}: { + onClose: () => void + disableSwipeToDismiss?: boolean + size?: 'sm' | 'md' +}) => { const fontSize = useSelector(state => state.fontSize) - const padding = fontSize / 2 + 2 + const padding = (fontSize / 2 + 2) / (size === 'sm' ? 2 : 1) return ( diff --git a/src/components/Popup.tsx b/src/components/Popup.tsx index c437f333b0..b0c6830140 100644 --- a/src/components/Popup.tsx +++ b/src/components/Popup.tsx @@ -1,49 +1,23 @@ -import React, { PropsWithChildren } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import React from 'react' +import { useSelector } from 'react-redux' import { css } from '../../styled-system/css' import { SystemStyleObject } from '../../styled-system/types' -import { alertActionCreator as alert } from '../actions/alert' -import { clearMulticursorsActionCreator as clearMulticursors } from '../actions/clearMulticursors' -import { deleteResumableFile } from '../actions/importFiles' -import { isTouch } from '../browser' -import { AlertType } from '../constants' -import useCombinedRefs from '../hooks/useCombinedRefs' -import usePositionFixed from '../hooks/usePositionFixed' -import useSwipeToDismiss from '../hooks/useSwipeToDismiss' -import syncStatusStore from '../stores/syncStatus' -import fastClick from '../util/fastClick' -import CloseButton from './CloseButton' +import PopupBase, { PopupBaseProps } from './PopupBase' /** A popup component that can be dismissed. */ const Popup = React.forwardRef< HTMLDivElement, - PropsWithChildren<{ - // used to cancel imports - importFileId?: string - /** If defined, will show a small x in the upper right corner. */ - onClose?: () => void + { textAlign?: 'center' | 'left' | 'right' value?: string | null cssRaw?: SystemStyleObject - }> ->(({ children, importFileId, onClose, textAlign = 'center', cssRaw }, ref) => { - const dispatch = useDispatch() - - const fontSize = useSelector(state => state.fontSize) + } & Omit +>(({ children, importFileId, onClose, textAlign = 'center', cssRaw, style, ...props }, ref) => { const padding = useSelector(state => state.fontSize / 2 + 2) - const multicursor = useSelector(state => state.alert?.alertType === AlertType.MulticursorActive) - const positionFixedStyles = usePositionFixed() - const useSwipeToDismissProps = useSwipeToDismiss({ - // dismiss after animation is complete to avoid touch events going to the Toolbar - onDismissEnd: () => { - dispatch(alert(null)) - }, - }) - - const combinedRefs = useCombinedRefs(isTouch ? [useSwipeToDismissProps.ref, ref] : [ref]) return ( -
{children}
- {importFileId && ( -
{ - deleteResumableFile(importFileId!) - syncStatusStore.update({ importProgress: 1 }) - onClose?.() - }} - > - cancel - - )} - {multicursor && ( - { - dispatch(clearMulticursors()) - onClose?.() - })} - > - cancel - - )} - {onClose ? : null} -
+ ) }) diff --git a/src/components/PopupBase.tsx b/src/components/PopupBase.tsx new file mode 100644 index 0000000000..e670759e21 --- /dev/null +++ b/src/components/PopupBase.tsx @@ -0,0 +1,86 @@ +import React, { PropsWithChildren } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { alertActionCreator as alert } from '../actions/alert' +import { clearMulticursorsActionCreator as clearMulticursors } from '../actions/clearMulticursors' +import { deleteResumableFile } from '../actions/importFiles' +import { isTouch } from '../browser' +import { AlertType } from '../constants' +import useCombinedRefs from '../hooks/useCombinedRefs' +import usePositionFixed from '../hooks/usePositionFixed' +import useSwipeToDismiss from '../hooks/useSwipeToDismiss' +import syncStatusStore from '../stores/syncStatus' +import fastClick from '../util/fastClick' +import CloseButton from './CloseButton' + +export type PopupBaseProps = PropsWithChildren<{ + // used to cancel imports + importFileId?: string + /** If defined, will show a small x in the upper right corner. */ + onClose?: () => void + disableTop?: boolean + className?: string + style?: React.CSSProperties + closeButtonSize?: 'sm' | 'md' +}> + +/** A popup component that can be dismissed. */ +const PopupBase = React.forwardRef( + ({ children, importFileId, onClose, className, style, disableTop = false, closeButtonSize }, ref) => { + const dispatch = useDispatch() + + const fontSize = useSelector(state => state.fontSize) + const multicursor = useSelector(state => state.alert?.alertType === AlertType.MulticursorActive) + const positionFixedStyles = usePositionFixed({ disableTop }) + const useSwipeToDismissProps = useSwipeToDismiss({ + // dismiss after animation is complete to avoid touch events going to the Toolbar + onDismissEnd: () => { + dispatch(alert(null)) + }, + }) + + const combinedRefs = useCombinedRefs(isTouch ? [useSwipeToDismissProps.ref, ref] : [ref]) + + return ( +
+ {children} + {importFileId && ( + { + deleteResumableFile(importFileId!) + syncStatusStore.update({ importProgress: 1 }) + onClose?.() + }} + > + cancel + + )} + {multicursor && ( + { + dispatch(clearMulticursors()) + onClose?.() + })} + > + cancel + + )} + {onClose ? : null} +
+ ) + }, +) + +PopupBase.displayName = 'Popup' + +export default PopupBase diff --git a/src/hooks/usePositionFixed.ts b/src/hooks/usePositionFixed.ts index 79d01607ea..175a7d604b 100644 --- a/src/hooks/usePositionFixed.ts +++ b/src/hooks/usePositionFixed.ts @@ -25,10 +25,14 @@ const initEventHandler = once(() => { }) /** Emulates position fixed on mobile Safari with positon absolute. Returns { position, overflowX, top } in absolute mode. */ -const usePositionFixed = (): { +const usePositionFixed = ({ + disableTop, +}: { + disableTop?: boolean +} = {}): { position: 'fixed' | 'absolute' overflowX?: 'hidden' | 'visible' - top: string + top?: string } => { const position = positionFixedStore.useState() const scrollTop = useScrollTop({ disabled: position === 'fixed' }) @@ -39,7 +43,7 @@ const usePositionFixed = (): { position: position ?? 'fixed', overflowX: position === 'absolute' ? 'hidden' : 'visible', /* spacing.safeAreaTop applies for rounded screens */ - top: position === 'absolute' ? `${scrollTop}px` : token('spacing.safeAreaTop'), + top: disableTop ? undefined : position === 'absolute' ? `${scrollTop}px` : token('spacing.safeAreaTop'), } }