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
6 changes: 4 additions & 2 deletions fixtures/view-transition/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ if (process.env.NODE_ENV === 'development') {
for (var key in require.cache) {
delete require.cache[key];
}
import('./render.js').then(({default: render}) => {
import('./render.js').then(mod => {
const render = mod.default.__esModule ? mod.default.default : mod.default;
render(req.url, res);
});
});
} else {
import('./render.js').then(({default: render}) => {
import('./render.js').then(mod => {
const render = mod.default.__esModule ? mod.default.default : mod.default;
app.get('/', function (req, res) {
render(req.url, res);
});
Expand Down
238 changes: 238 additions & 0 deletions fixtures/view-transition/src/components/NestedExit.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
.nested-exit-demo {
width: 300px;
min-height: 280px;
background: #f5f5f5;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
}

.feed-item {
background: #fff;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 8px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.feed-item:hover {
background: #f0f0f0;
}

.feed-item h3 {
margin: 0 0 4px;
font-size: 16px;
}

.feed-item p {
margin: 0;
font-size: 13px;
color: #666;
}

.detail-view {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.detail-view h3 {
margin: 0 0 4px;
font-size: 16px;
}

.detail-view p {
margin: 0;
font-size: 13px;
color: #666;
}

.back-button {
background: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
}

/* Directional exit: posts above go up, posts below go down */
@keyframes nested-exit-up {
from {
opacity: 1;
translate: 0 0;
}
to {
opacity: 0;
translate: 0 -60px;
}
}

@keyframes nested-exit-down {
from {
opacity: 1;
translate: 0 0;
}
to {
opacity: 0;
translate: 0 60px;
}
}

::view-transition-old(.nested-exit-up):only-child {
animation: nested-exit-up 600ms ease-out forwards;
}

::view-transition-old(.nested-exit-down):only-child {
animation: nested-exit-down 600ms ease-out forwards;
}

/* Forward shared: delayed until exits finish */
::view-transition-group(.nested-shared-post-forward) {
animation-duration: 700ms;
animation-delay: 300ms;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}

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

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

/* Back shared: starts immediately, then items enter after */
::view-transition-group(.nested-shared-post-back) {
animation-duration: 700ms;
animation-timing-function: ease-in-out;
}

/* Inner shared elements (title, body) start after card begins growing */
::view-transition-group(.nested-shared-inner-forward) {
animation-duration: 600ms;
animation-delay: 450ms;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}

::view-transition-old(.nested-shared-inner-forward) {
animation-delay: 450ms;
animation-duration: 600ms;
animation-fill-mode: both;
}

::view-transition-new(.nested-shared-inner-forward) {
animation-delay: 450ms;
animation-duration: 600ms;
animation-fill-mode: both;
}

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

/* Back button slides in from left */
@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 700ms both;
}

::view-transition-old(.nested-back-btn-exit):only-child {
animation: nested-back-btn-exit 200ms ease-in forwards;
}

/* Extra detail content fades in/out */
@keyframes nested-extra-enter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

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

::view-transition-old(.nested-extra-exit):only-child {
animation: nested-extra-enter 200ms ease-in reverse forwards;
}

/* Directional enter: items fly back in after shared transition finishes */
@keyframes nested-enter-from-up {
from {
opacity: 0;
translate: 0 -60px;
}
to {
opacity: 1;
translate: 0 0;
}
}

@keyframes nested-enter-from-down {
from {
opacity: 0;
translate: 0 60px;
}
to {
opacity: 1;
translate: 0 0;
}
}

::view-transition-new(.nested-enter-from-up):only-child {
animation: nested-enter-from-up 600ms ease-out 700ms both;
}

::view-transition-new(.nested-enter-from-down):only-child {
animation: nested-enter-from-down 600ms ease-out 700ms both;
}

/* Enter animation for detail view (when no shared match) */
@keyframes nested-enter-detail {
from {
opacity: 0;
translate: 0 30px;
}
to {
opacity: 1;
translate: 0 0;
}
}

::view-transition-new(.nested-enter-detail):only-child {
animation: nested-enter-detail 600ms ease-out both;
}
136 changes: 136 additions & 0 deletions fixtures/view-transition/src/components/NestedExit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, {
ViewTransition,
useState,
startTransition,
addTransitionType,
} from 'react';

import './NestedExit.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 FeedItem({item, index, onSelect}) {
// Build exit/enter maps: for each possible clicked item, determine direction
const exitMap = {};
const enterMap = {};
items.forEach((_, otherIndex) => {
if (otherIndex !== index) {
const key = 'select-' + otherIndex;
exitMap[key] = index < otherIndex ? 'nested-exit-up' : 'nested-exit-down';
enterMap[key] =
index < otherIndex ? 'nested-enter-from-up' : 'nested-enter-from-down';
}
});

const shareInner = {
'nav-forward': 'nested-shared-inner-forward',
'nav-back': 'nested-shared-inner-back',
};

return (
<ViewTransition
name={'nested-post-' + item.id}
share={{
'nav-forward': 'nested-shared-post-forward',
'nav-back': 'nested-shared-post-back',
}}
exit={exitMap}
enter={enterMap}>
<div className="feed-item" onClick={() => onSelect(item, index)}>
<ViewTransition name={'nested-title-' + item.id} share={shareInner}>
<h3>{item.title}</h3>
</ViewTransition>
<ViewTransition name={'nested-body-' + item.id} share={shareInner}>
<p>{item.body}</p>
</ViewTransition>
</div>
</ViewTransition>
);
}

function Detail({item, onBack}) {
const shareInner = {
'nav-forward': 'nested-shared-inner-forward',
'nav-back': 'nested-shared-inner-back',
};

return (
<ViewTransition
name={'nested-post-' + item.id}
share={{
'nav-forward': 'nested-shared-post-forward',
'nav-back': 'nested-shared-post-back',
}}
enter={{'permalink-navigation': 'nested-enter-detail'}}>
<div className="detail-view">
<ViewTransition
enter={{'nav-forward': 'nested-back-btn-enter'}}
exit={{'nav-back': 'nested-back-btn-exit'}}>
<button className="back-button" onClick={onBack}>
← Back
</button>
</ViewTransition>
<ViewTransition name={'nested-title-' + item.id} share={shareInner}>
<h3>{item.title}</h3>
</ViewTransition>
<ViewTransition name={'nested-body-' + item.id} share={shareInner}>
<p>{item.body}</p>
</ViewTransition>
<ViewTransition
enter={{'nav-forward': 'nested-extra-enter'}}
exit={{'nav-back': 'nested-extra-exit'}}>
<p>This is the detail view with more content.</p>
</ViewTransition>
</div>
</ViewTransition>
);
}

export default function NestedExit() {
const [selected, setSelected] = useState(null);

function selectItem(item, clickedIndex) {
startTransition(() => {
addTransitionType('permalink-navigation');
addTransitionType('nav-forward');
addTransitionType('select-' + clickedIndex);
setSelected(item);
});
}

function goBack() {
const backIndex = items.findIndex(i => i.id === selected.id);
startTransition(() => {
addTransitionType('permalink-navigation');
addTransitionType('nav-back');
addTransitionType('select-' + backIndex);
setSelected(null);
});
}

return (
<div className="nested-exit-demo">
<h3>Nested Exit/Enter</h3>
<ViewTransition key={selected ? 'detail' : 'feed'}>
{selected ? (
<Detail item={selected} onBack={goBack} />
) : (
<div>
{items.map((item, index) => (
<FeedItem
key={item.id}
item={item}
index={index}
onSelect={selectItem}
/>
))}
</div>
)}
</ViewTransition>
</div>
);
}
Loading
Loading