Skip to content

Commit

Permalink
extract out PopupBase and use it in Alert + Popup
Browse files Browse the repository at this point in the history
  • Loading branch information
yangchristina committed Jan 2, 2025
1 parent bfd56e3 commit ccd0e60
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 81 deletions.
8 changes: 2 additions & 6 deletions panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,8 @@ export default defineConfig({
},
},
fontSizes: {
sm: {
value: '80%',
},
md: {
value: '90%',
},
sm: { value: '80%' },
md: { value: '90%' },
},
spacing: {
modalPadding: { value: '8%' },
Expand Down
20 changes: 17 additions & 3 deletions src/components/Alert.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand All @@ -38,7 +49,8 @@ const Alert: FC = () => {
{alert ? (
<FadeTransition duration='slow' nodeRef={popupRef} onEntering={() => 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. */}
<div
<PopupBase
disableTop
className={css({
position: 'fixed',
boxSizing: 'border-box',
Expand All @@ -59,10 +71,12 @@ const Alert: FC = () => {
ref={popupRef}
key={value}
data-testid='alert-content'
onClose={onClose}
closeButtonSize='sm'
>
{renderedIcon}
{value}
</div>
</PopupBase>
</FadeTransition>
) : null}
</TransitionGroup>
Expand Down
17 changes: 11 additions & 6 deletions src/components/CloseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<a
{...fastClick(onClose)}
className={cx(
upperRightRecipe(),
css({
fontSize: 'sm',
color: 'inherit',
right: '0',
textDecoration: 'none',
top: '0',
}),
)}
style={{ fontSize, padding: `${padding}px ${padding * 1.25}px` }}
style={{ fontSize: size === 'sm' ? fontSize / 2 : fontSize, padding: `${padding}px ${padding * 1.25}px` }}
aria-label={disableSwipeToDismiss ? 'no-swipe-to-dismiss' : undefined}
data-testid='close-button'
>
Expand Down
74 changes: 11 additions & 63 deletions src/components/Popup.tsx
Original file line number Diff line number Diff line change
@@ -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<PopupBaseProps, 'className'>
>(({ 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 (
<div
<PopupBase
ref={ref}
className={css(
{
boxShadow: 'none',
Expand All @@ -61,44 +35,18 @@ const Popup = React.forwardRef<
},
cssRaw,
)}
{...(isTouch ? useSwipeToDismissProps : null)}
ref={combinedRefs}
// merge style with useSwipeToDismissProps.style (transform, transition, and touchAction for sticking to user's touch)
style={{
...positionFixedStyles,
fontSize,
// scale with font size to stay vertically centered over toolbar
padding: `${padding}px 0 ${padding}px`,
textAlign,
...(isTouch ? useSwipeToDismissProps.style : null),
...style,
}}
{...props}
>
<div data-testid='popup-value' className={css({ padding: '0.25em', backgroundColor: 'bgOverlay80' })}>
{children}
</div>
{importFileId && (
<a
onClick={() => {
deleteResumableFile(importFileId!)
syncStatusStore.update({ importProgress: 1 })
onClose?.()
}}
>
cancel
</a>
)}
{multicursor && (
<a
{...fastClick(() => {
dispatch(clearMulticursors())
onClose?.()
})}
>
cancel
</a>
)}
{onClose ? <CloseButton onClose={onClose} disableSwipeToDismiss /> : null}
</div>
</PopupBase>
)
})

Expand Down
86 changes: 86 additions & 0 deletions src/components/PopupBase.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, PopupBaseProps>(
({ 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 (
<div
className={className}
{...(isTouch ? useSwipeToDismissProps : null)}
ref={combinedRefs}
// merge style with useSwipeToDismissProps.style (transform, transition, and touchAction for sticking to user's touch)
style={{
...positionFixedStyles,
fontSize,
...(isTouch ? useSwipeToDismissProps.style : null),
...style,
}}
>
{children}
{importFileId && (
<a
onClick={() => {
deleteResumableFile(importFileId!)
syncStatusStore.update({ importProgress: 1 })
onClose?.()
}}
>
cancel
</a>
)}
{multicursor && (
<a
{...fastClick(() => {
dispatch(clearMulticursors())
onClose?.()
})}
>
cancel
</a>
)}
{onClose ? <CloseButton size={closeButtonSize} onClose={onClose} disableSwipeToDismiss /> : null}
</div>
)
},
)

PopupBase.displayName = 'Popup'

export default PopupBase
10 changes: 7 additions & 3 deletions src/hooks/usePositionFixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand All @@ -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'),
}
}

Expand Down

0 comments on commit ccd0e60

Please sign in to comment.