mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
feat(component): mobile menu support (#7892)
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCallback, useState } from 'react';
|
||||
import { Button } from '../button';
|
||||
import type { InputProps } from '../input';
|
||||
import { Input } from '../input';
|
||||
import { RadioGroup } from '../radio';
|
||||
import type { ConfirmModalProps } from './confirm-modal';
|
||||
import { ConfirmModal } from './confirm-modal';
|
||||
import type { ModalProps } from './modal';
|
||||
@@ -105,3 +106,36 @@ export const Confirm: StoryFn<ModalProps> =
|
||||
|
||||
export const Overlay: StoryFn<ModalProps> =
|
||||
OverlayModalTemplate.bind(undefined);
|
||||
|
||||
export const Animations = () => {
|
||||
const animations = ['fadeScaleTop', 'slideBottom', 'none'];
|
||||
const [open, setOpen] = useState(false);
|
||||
const [animation, setAnimation] =
|
||||
useState<ModalProps['animation']>('fadeScaleTop');
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<RadioGroup
|
||||
value={animation}
|
||||
onChange={setAnimation}
|
||||
items={animations}
|
||||
/>
|
||||
<Button onClick={() => setOpen(true)}>Open dialog</Button>
|
||||
<Modal
|
||||
contentWrapperStyle={
|
||||
animation === 'slideBottom'
|
||||
? {
|
||||
alignItems: 'end',
|
||||
padding: 10,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
animation={animation}
|
||||
>
|
||||
This is a dialog with animation: {animation}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,9 +9,10 @@ import * as Dialog from '@radix-ui/react-dialog';
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CSSProperties, MouseEvent } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { isMobile } from '../../utils/env';
|
||||
import type { IconButtonProps } from '../button';
|
||||
import { IconButton } from '../button';
|
||||
import * as styles from './styles.css';
|
||||
@@ -32,6 +33,12 @@ export interface ModalProps extends DialogProps {
|
||||
contentOptions?: DialogContentProps;
|
||||
overlayOptions?: DialogOverlayProps;
|
||||
closeButtonOptions?: IconButtonProps;
|
||||
contentWrapperClassName?: string;
|
||||
contentWrapperStyle?: CSSProperties;
|
||||
/**
|
||||
* @default 'fadeScaleTop'
|
||||
*/
|
||||
animation?: 'fadeScaleTop' | 'none' | 'slideBottom';
|
||||
}
|
||||
type PointerDownOutsideEvent = Parameters<
|
||||
Exclude<DialogContentProps['onPointerDownOutside'], undefined>
|
||||
@@ -128,10 +135,14 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
overlayOptions: {
|
||||
className: overlayClassName,
|
||||
style: overlayStyle,
|
||||
onClick: onOverlayClick,
|
||||
...otherOverlayOptions
|
||||
} = {},
|
||||
closeButtonOptions,
|
||||
children,
|
||||
contentWrapperClassName,
|
||||
contentWrapperStyle,
|
||||
animation = 'fadeScaleTop',
|
||||
...otherProps
|
||||
} = props;
|
||||
const { className: closeButtonClassName, ...otherCloseButtonProps } =
|
||||
@@ -167,6 +178,18 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
[onEscapeKeyDown, persistent]
|
||||
);
|
||||
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
onOverlayClick?.(e);
|
||||
if (persistent) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
},
|
||||
[onOpenChange, onOverlayClick, persistent]
|
||||
);
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
@@ -180,13 +203,27 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
>
|
||||
<Dialog.Portal container={container} {...portalOptions}>
|
||||
<Dialog.Overlay
|
||||
className={clsx(styles.modalOverlay, overlayClassName)}
|
||||
className={clsx(
|
||||
`anim-${animation}`,
|
||||
styles.modalOverlay,
|
||||
overlayClassName,
|
||||
{ mobile: isMobile() }
|
||||
)}
|
||||
style={{
|
||||
...overlayStyle,
|
||||
}}
|
||||
onClick={handleOverlayClick}
|
||||
{...otherOverlayOptions}
|
||||
/>
|
||||
<div data-modal={modal} className={clsx(styles.modalContentWrapper)}>
|
||||
<div
|
||||
data-modal={modal}
|
||||
className={clsx(
|
||||
`anim-${animation}`,
|
||||
styles.modalContentWrapper,
|
||||
contentWrapperClassName
|
||||
)}
|
||||
style={contentWrapperStyle}
|
||||
>
|
||||
<Dialog.Content
|
||||
onPointerDownOutside={handlePointerDownOutSide}
|
||||
onEscapeKeyDown={handleEscapeKeyDown}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
createVar,
|
||||
generateIdentifier,
|
||||
@@ -18,7 +19,7 @@ const overlayShow = keyframes({
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
const contentShow = keyframes({
|
||||
const contentShowFadeScaleTop = keyframes({
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(-2%) scale(0.96)',
|
||||
@@ -28,7 +29,7 @@ const contentShow = keyframes({
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
});
|
||||
export const contentHide = keyframes({
|
||||
const contentHideFadeScaleTop = keyframes({
|
||||
to: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(-2%) scale(0.96)',
|
||||
@@ -38,15 +39,35 @@ export const contentHide = keyframes({
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
});
|
||||
|
||||
const contentShowSlideBottom = keyframes({
|
||||
from: { transform: 'translateY(100%)' },
|
||||
to: { transform: 'translateY(0)' },
|
||||
});
|
||||
const contentHideSlideBottom = keyframes({
|
||||
from: { transform: 'translateY(0)' },
|
||||
to: { transform: 'translateY(100%)' },
|
||||
});
|
||||
const modalContentViewTransitionNameFadeScaleTop = generateIdentifier(
|
||||
'modal-content-fade-scale-top'
|
||||
);
|
||||
const modalContentViewTransitionNameSlideBottom = generateIdentifier(
|
||||
'modal-content-slide-bottom'
|
||||
);
|
||||
export const modalOverlay = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: cssVar('backgroundModalColor'),
|
||||
zIndex: cssVar('zIndexModal'),
|
||||
animation: `${overlayShow} 150ms forwards`,
|
||||
selectors: {
|
||||
'&.anim-none': {
|
||||
animation: 'none',
|
||||
},
|
||||
'&.mobile': {
|
||||
backgroundColor: cssVarV2('layer/mobile/modal'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const modalContentViewTransitionName = generateIdentifier('modal-content');
|
||||
export const modalContentWrapper = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
@@ -54,14 +75,37 @@ export const modalContentWrapper = style({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: cssVar('zIndexModal'),
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
viewTransitionName: modalContentViewTransitionName,
|
||||
});
|
||||
globalStyle(`::view-transition-old(${modalContentViewTransitionName})`, {
|
||||
animation: `${contentHide} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
|
||||
selectors: {
|
||||
'&.anim-none': {
|
||||
animation: 'none',
|
||||
},
|
||||
'&.anim-fadeScaleTop': {
|
||||
animation: `${contentShowFadeScaleTop} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
viewTransitionName: modalContentViewTransitionNameFadeScaleTop,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
'&.anim-slideBottom': {
|
||||
animation: `${contentShowSlideBottom} 0.23s ease`,
|
||||
viewTransitionName: modalContentViewTransitionNameSlideBottom,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(
|
||||
`::view-transition-old(${modalContentViewTransitionNameFadeScaleTop})`,
|
||||
{
|
||||
animation: `${contentHideFadeScaleTop} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`::view-transition-old(${modalContentViewTransitionNameSlideBottom})`,
|
||||
{
|
||||
animation: `${contentHideSlideBottom} 0.23s ease`,
|
||||
animationFillMode: 'forwards',
|
||||
}
|
||||
);
|
||||
|
||||
export const modalContent = style({
|
||||
vars: {
|
||||
@@ -72,6 +116,8 @@ export const modalContent = style({
|
||||
width: widthVar,
|
||||
height: heightVar,
|
||||
minHeight: minHeightVar,
|
||||
maxHeight: 'calc(100vh - 32px)',
|
||||
maxWidth: 'calc(100vw - 20px)',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: cssVar('fontBase'),
|
||||
fontWeight: '400',
|
||||
@@ -81,7 +127,6 @@ export const modalContent = style({
|
||||
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
||||
boxShadow: cssVar('popoverShadow'),
|
||||
borderRadius: '12px',
|
||||
maxHeight: 'calc(100vh - 32px)',
|
||||
// :focus-visible will set outline
|
||||
outline: 'none',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user