-
Notifications
You must be signed in to change notification settings - Fork 5.5k
feat: page transitions #40588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: page transitions #40588
Changes from all commits
0a04ed4
1b9ca59
ec4c374
4f618e1
4a75b8b
55551f0
0cfbae3
6e3d7dc
fa33e27
5b70ca0
ee346ab
8f8485e
120880f
420ff23
9df507d
9ea84c9
0d6c7bf
4ad11e1
d5513c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { getBrowserName } from '../../../shared/modules/browser-runtime.utils'; | ||
| import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; | ||
|
|
||
| const isTransitionSupported = () => { | ||
| if (process.env.IN_TEST || getBrowserName() === PLATFORM_FIREFOX) { | ||
| return false; | ||
| } | ||
|
|
||
| return Boolean(document.startViewTransition); | ||
| }; | ||
|
|
||
| const transitionSupported = isTransitionSupported(); | ||
| type TransitionCallback = () => void | Promise<void>; | ||
|
|
||
| const startTransition = ( | ||
| direction: 'forward' | 'back', | ||
| callback: TransitionCallback, | ||
| ) => { | ||
| if (!transitionSupported) { | ||
| callback(); | ||
| return; | ||
| } | ||
| document.documentElement.dataset.pageTransition = direction; | ||
| const transition = document.startViewTransition(callback); | ||
| transition.finished.finally(() => { | ||
| delete document.documentElement.dataset.pageTransition; | ||
| }); | ||
| }; | ||
|
|
||
| export const transitionForward = (callback: TransitionCallback) => { | ||
| startTransition('forward', callback); | ||
| }; | ||
|
|
||
| export const transitionBack = (callback: TransitionCallback) => { | ||
| startTransition('back', callback); | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate view transition utility pattern across filesLow Severity The new Triggered by project rule: BUGBOT Rules |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* Specs */ | ||
| :root { | ||
| --page-transition-duration: 180ms; | ||
| --page-transition-ease-out: cubic-bezier(0.4, 0, 0.2, 1); | ||
| --page-transition-ease-in: cubic-bezier(0.4, 0, 1, 1); | ||
| --page-scale-distance: 0.97; | ||
| } | ||
|
|
||
| /* Clip overflow and make size changes instant (don't animate height) */ | ||
| ::view-transition-group(root) { | ||
| overflow: hidden; | ||
| animation-duration: 0s; | ||
| } | ||
|
|
||
| /* Disable default crossfade blending */ | ||
| ::view-transition-image-pair(root) { | ||
| isolation: auto; | ||
| } | ||
|
|
||
| /* Avoid ghosting */ | ||
| ::view-transition-old(root), | ||
| ::view-transition-new(root) { | ||
| mix-blend-mode: normal; | ||
| } | ||
|
|
||
| /* Forward: old fades out, new fades in + scale up */ | ||
| :root[data-page-transition='forward']::view-transition-old(root) { | ||
| animation: fade-out var(--page-transition-duration) var(--page-transition-ease-in) both; | ||
| } | ||
|
|
||
| :root[data-page-transition='forward']::view-transition-new(root) { | ||
| animation: page-enter var(--page-transition-duration) var(--page-transition-ease-out) both; | ||
| } | ||
|
|
||
| /* Back: old fades out + scale down, new fades in */ | ||
| :root[data-page-transition='back']::view-transition-old(root) { | ||
| animation: page-exit var(--page-transition-duration) var(--page-transition-ease-in) both; | ||
| } | ||
|
|
||
| :root[data-page-transition='back']::view-transition-new(root) { | ||
| animation: fade-in var(--page-transition-duration) var(--page-transition-ease-out) both; | ||
| } | ||
|
|
||
| @keyframes fade-out { | ||
| from { opacity: 1; } | ||
| to { opacity: 0; } | ||
| } | ||
|
|
||
| @keyframes fade-in { | ||
| from { opacity: 0; } | ||
| to { opacity: 1; } | ||
| } | ||
|
|
||
| @keyframes page-enter { | ||
| from { | ||
| opacity: 0; | ||
| transform: scale(var(--page-scale-distance)); | ||
| } | ||
|
|
||
| to { | ||
| opacity: 1; | ||
| transform: scale(1); | ||
| } | ||
| } | ||
|
|
||
| @keyframes page-exit { | ||
| from { | ||
| opacity: 1; | ||
| transform: scale(1); | ||
| } | ||
|
|
||
| to { | ||
| opacity: 0; | ||
| transform: scale(var(--page-scale-distance)); | ||
| } | ||
| } | ||
|
|
||
| /* Respect reduced motion preference */ | ||
| @media (prefers-reduced-motion: reduce) { | ||
| ::view-transition-old(root), | ||
| ::view-transition-new(root) { | ||
| animation-duration: 0.01ms !important; | ||
| } | ||
|
|
||
| .page-enter-animation, | ||
| .page-exit-animation { | ||
| animation-duration: 0.01ms !important; | ||
| } | ||
| } | ||
|
|
||
| /* Modal enter/exit animations (used by receive-modal) */ | ||
| .page-enter-animation { | ||
| animation: page-enter var(--page-transition-duration) var(--page-transition-ease-out) both; | ||
| } | ||
|
|
||
| .page-exit-animation { | ||
| animation: page-exit var(--page-transition-duration) var(--page-transition-ease-in) both; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ import { useQuoteFetchEvents } from '../../hooks/bridge/useQuoteFetchEvents'; | |
| import { TextVariant } from '../../helpers/constants/design-system'; | ||
| import { useTxAlerts } from '../../hooks/bridge/useTxAlerts'; | ||
| import { getFromChain, getBridgeQuotes } from '../../ducks/bridge/selectors'; | ||
| import { transitionBack } from '../../components/ui/transition'; | ||
| import PrepareBridgePage from './prepare/prepare-bridge-page'; | ||
| import AwaitingSignaturesCancelButton from './awaiting-signatures/awaiting-signatures-cancel-button'; | ||
| import AwaitingSignatures from './awaiting-signatures/awaiting-signatures'; | ||
|
|
@@ -103,13 +104,17 @@ const CrossChainSwap = () => { | |
| // Sets tx alerts for the active quote | ||
| useTxAlerts(); | ||
|
|
||
| const redirectToDefaultRoute = async () => { | ||
| await resetControllerAndInputStates(); | ||
| if (isFromTransactionShield) { | ||
| navigate(TRANSACTION_SHIELD_ROUTE); | ||
| } else { | ||
| navigate(DEFAULT_ROUTE, { state: { stayOnHomePage: true } }); | ||
| } | ||
| const redirectToDefaultRoute = () => { | ||
| const doNavigate = async () => { | ||
| await resetControllerAndInputStates(); | ||
| if (isFromTransactionShield) { | ||
| navigate(TRANSACTION_SHIELD_ROUTE); | ||
| } else { | ||
| navigate(DEFAULT_ROUTE, { state: { stayOnHomePage: true } }); | ||
| } | ||
| }; | ||
|
|
||
| transitionBack(() => doNavigate().catch(() => undefined)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bridge state reset errors silently swallowedMedium Severity The |
||
| }; | ||
|
|
||
| const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modal stuck open when getAnimations unavailable and animation missing
Low Severity
In
handleClose, whentypeof el.getAnimations !== 'function', therequestAnimationFramecallback returns early without callingcloseOnce. This safety-net path is meant to detect when no animation actually started and close the modal anyway. IfgetAnimationsis unsupported and the CSS animation also fails to fireanimationend/animationcancel(e.g., underprefers-reduced-motionwith near-zero duration completing before listeners bind), the modal will never close.