Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions fixtures/view-transition/src/components/NestedParentExit.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
.nested-parent-exit {
width: 280px;
min-height: 280px;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #ccc;
}

.nested-parent-exit-swipe {
min-height: 200px;
}

.nested-parent-exit-label {
margin: 0 0 0.75rem;
font-size: 13px;
color: #666;
}

.nested-parent-exit-panel {
min-height: 240px;
}

.nested-parent-exit .feed-item {
margin-bottom: 0.75rem;
cursor: pointer;
}

.nested-parent-exit .feed-item-title {
margin: 0 0 0.15rem;
font-size: 15px;
font-weight: 600;
}

.nested-parent-exit .feed-item p,
.nested-parent-exit .detail-view p {
margin: 0;
font-size: 13px;
color: #666;
}

.nested-parent-exit .back-button {
margin-bottom: 0.75rem;
padding: 0;
border: none;
background: none;
cursor: pointer;
font: inherit;
color: inherit;
text-decoration: underline;
}

@keyframes nested-exit-left {
from {
opacity: 1;
translate: 0 0;
}
to {
opacity: 0;
translate: -120% 0;
}
}

::view-transition-old(.nested-exit-left) {
animation: nested-exit-left 450ms ease-out forwards;
}

@keyframes nested-enter-from-left {
from {
opacity: 0;
translate: -120% 0;
}
to {
opacity: 1;
translate: 0 0;
}
}

::view-transition-new(.nested-enter-from-left) {
animation: nested-enter-from-left 450ms ease-out 650ms both;
}

::view-transition-group(.nested-shared-post-forward) {
animation-duration: 600ms;
animation-delay: 300ms;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}

::view-transition-old(.nested-shared-post-forward),
::view-transition-new(.nested-shared-post-forward) {
animation-delay: 300ms;
animation-duration: 600ms;
animation-fill-mode: both;
}

::view-transition-group(.nested-shared-post-back) {
animation-duration: 600ms;
animation-timing-function: ease-in-out;
}

::view-transition-group(.nested-shared-inner-forward) {
animation-duration: 500ms;
animation-delay: 400ms;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}

::view-transition-old(.nested-shared-inner-forward),
::view-transition-new(.nested-shared-inner-forward) {
animation-delay: 400ms;
animation-duration: 500ms;
animation-fill-mode: both;
}

::view-transition-group(.nested-shared-inner-back) {
animation-duration: 500ms;
animation-delay: 100ms;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}

@keyframes nested-back-btn-enter {
from {
opacity: 0;
translate: -20px 0;
}
to {
opacity: 1;
translate: 0 0;
}
}

@keyframes nested-back-btn-exit {
from {
opacity: 1;
translate: 0 0;
}
to {
opacity: 0;
translate: -20px 0;
}
}

::view-transition-new(.nested-back-btn-enter):only-child {
animation: nested-back-btn-enter 300ms ease-out 650ms both;
}

::view-transition-old(.nested-back-btn-exit):only-child {
animation: nested-back-btn-exit 200ms ease-in forwards;
}
174 changes: 174 additions & 0 deletions fixtures/view-transition/src/components/NestedParentExit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, {
ViewTransition,
useState,
useOptimistic,
startTransition,
addTransitionType,
} from 'react';
import SwipeRecognizer from './SwipeRecognizer.js';
import './NestedParentExit.css';

const items = [
{id: 1, title: 'First Post', body: 'Hello from the first post.'},
{id: 2, title: 'Second Post', body: 'Hello from the second post.'},
{id: 3, title: 'Third Post', body: 'Hello from the third post.'},
];

function logGestureParent(kind, title, _timeline, _options, _instance, types) {
// eslint-disable-next-line no-console
console.log(`[NestedParentExit] onGestureParent${kind}`, title, types);
}

function FeedItem({item, index, activeIndex, onSelect}) {
const isActive = activeIndex === index;

return (
<ViewTransition
name={'nested-post-' + item.id}
share={{
'nav-forward': 'nested-shared-post-forward',
'nav-back': 'nested-shared-post-back',
}}
parentExit={isActive ? undefined : 'nested-exit-left'}
parentEnter={isActive ? undefined : 'nested-enter-from-left'}
onGestureParentExit={(...args) =>
logGestureParent('Exit', item.title, ...args)
}
onGestureParentEnter={(...args) =>
logGestureParent('Enter', item.title, ...args)
}>
<div className="feed-item" onClick={() => onSelect(item, index)}>
<ViewTransition
name={'nested-title-' + item.id}
share={{
'nav-forward': 'nested-shared-inner-forward',
'nav-back': 'nested-shared-inner-back',
}}>
<div className="feed-item-title">{item.title}</div>
</ViewTransition>
<p>{item.body}</p>
</div>
</ViewTransition>
);
}

function Detail({item, onBack}) {
return (
<ViewTransition
name={'nested-post-' + item.id}
share={{
'nav-forward': 'nested-shared-post-forward',
'nav-back': 'nested-shared-post-back',
}}>
<div className="detail-view">
<ViewTransition
enter="nested-back-btn-enter"
exit="nested-back-btn-exit">
<button className="back-button" onClick={onBack}>
← Back
</button>
</ViewTransition>
<ViewTransition
name={'nested-title-' + item.id}
share={{
'nav-forward': 'nested-shared-inner-forward',
'nav-back': 'nested-shared-inner-back',
}}>
<div className="feed-item-title">{item.title}</div>
</ViewTransition>
<p>{item.body}</p>
</div>
</ViewTransition>
);
}

const initialNav = {selected: null, activeIndex: null};

export default function NestedParentExit() {
const [nav, setNav] = useState(initialNav);
const [optimisticNav, navigateByGesture] = useOptimistic(
nav,
(state, direction) => {
if (direction === 'left' && state.selected === null) {
return {selected: items[0], activeIndex: 0};
}
if (direction === 'right' && state.selected !== null) {
return {
selected: null,
activeIndex:
state.activeIndex ??
items.findIndex(i => i.id === state.selected.id),
};
}
return state;
}
);

const {selected, activeIndex} = optimisticNav;

function goToDetail(item, index) {
setNav({selected: item, activeIndex: index});
startTransition(() => {
addTransitionType('nav-forward');
});
}

function goBack() {
const current = selected;
if (current == null) {
return;
}
const backIndex = items.findIndex(i => i.id === current.id);
setNav({selected: null, activeIndex: backIndex});
startTransition(() => {
addTransitionType('nav-back');
});
}

function swipeAction() {
if (nav.selected === null) {
goToDetail(items[0], 0);
} else {
goBack();
}
}

return (
<div className="nested-parent-exit">
<p className="nested-parent-exit-label">
Parent Exit/Enter — click a post or swipe (scroll the strip below)
</p>
<div className="nested-parent-exit-swipe swipe-recognizer">
<SwipeRecognizer
action={swipeAction}
gesture={direction => {
addTransitionType(
direction === 'left' ? 'nav-forward' : 'nav-back'
);
navigateByGesture(direction);
}}
direction={selected ? 'right' : 'left'}>
<ViewTransition key={selected ? 'detail' : 'feed'} update="none">
<div className="nested-parent-exit-panel">
{selected ? (
<Detail item={selected} onBack={goBack} />
) : (
<>
{items.map((item, index) => (
<FeedItem
key={item.id}
item={item}
index={index}
activeIndex={activeIndex}
onSelect={goToDetail}
/>
))}
</>
)}
</div>
</ViewTransition>
</SwipeRecognizer>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import './Page.css';

import transitions from './Transitions.module.css';
import NestedReveal from './NestedReveal.js';
import NestedParentExit from './NestedParentExit.js';

async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -322,6 +323,7 @@ export default function Page({url, navigate}) {
</ViewTransition>
</SwipeRecognizer>
<NestedReveal />
<NestedParentExit />
</div>
);
}
Loading
Loading