Skip to content

Commit

Permalink
feat: support view transition
Browse files Browse the repository at this point in the history
  • Loading branch information
Daydreamer-riri committed Apr 5, 2024
1 parent e45907f commit a847816
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 96 deletions.
4 changes: 3 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ export default defineBuildConfig({
'src/hooks/useTransition.ts',
'src/hooks/useSwitchTransition/index.tsx',
'src/hooks/useListTransition.tsx',
'src/viewTransition.ts',
],
externals: ['react', 'react-dom'],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
esbuild: {
// minify: true,
minify: true,
},
},
})
12 changes: 7 additions & 5 deletions docs/components/BasicUseListTransition/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import { useRef, useState } from 'react'
import { getSimpleStatus, useListTransition } from 'transition-hooks'
import { useListTransition } from 'transition-hooks'
import { startViewTransition } from 'transition-hooks/viewTransition'
import { Button } from '../Button'
import { shuffle } from '../utils'

const numbers = Array.from({ length: 5 }, (_, i) => i)
export function BasicUseListTransition() {
const [list, setList] = useState(numbers)
const idRef = useRef(numbers.length)
const { transitionList } = useListTransition(list, { entered: false, timeout: 300, keyExtractor: i => i })
const { transitionList } = useListTransition(list, { entered: false, timeout: 300, keyExtractor: i => i, viewTransition: startViewTransition })

return (
<div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ display: 'flex', gap: 4, marginBottom: 8 }}>
<Button onClick={insert}>insert</Button>
<Button onClick={remove}>remove</Button>
<Button onClick={() => setList(shuffle)}>shuffle</Button>
</div>
<ul>
{transitionList((item, { status }) => {
const simpleStatus = getSimpleStatus(status)
{transitionList((item, { key, simpleStatus }) => {
return (
<li
style={{
position: simpleStatus === 'exit' ? 'absolute' : 'relative',
opacity: simpleStatus === 'enter' ? 1 : 0,
transform: simpleStatus === 'enter' ? 'translateX(0)' : 'translateX(20px)',
transition: 'all 300ms',
viewTransitionName: simpleStatus === 'enter' ? `transition-list-${key}` : '',
}}
>
- {item}
Expand Down
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./useTransition": {
"import": "./dist/hooks/useTransition.mjs",
"require": "./dist/hooks/useTransition.cjs"
},
"./useSwitchTransition": {
"import": "./dist/hooks/useSwitchTransition/index.mjs",
"require": "./dist/hooks/useSwitchTransition/index.cjs"
},
"./useListTransition": {
"import": "./dist/hooks/useListTransition.mjs",
"require": "./dist/hooks/useListTransition.cjs"
},
"./viewTransition": {
"import": "./dist/viewTransition.mjs",
"require": "./dist/viewTransition.cjs"
}
},
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -62,11 +78,13 @@
"@ririd/eslint-config": "^1.1.0",
"@types/node": "^18.15.11",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.24",
"bumpp": "^9.4.0",
"eslint": "^8.56.0",
"esno": "^4.7.0",
"lint-staged": "15.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "5.0.5",
"simple-git-hooks": "^2.11.1",
"transition-hooks": "workspace:*",
Expand Down
149 changes: 89 additions & 60 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

78 changes: 49 additions & 29 deletions src/hooks/useListTransition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Timeout } from '../helpers/getTimeout'
import { getTimeout } from '../helpers/getTimeout'
import useMemoizedFn from '../helpers/useMemorizeFn'

type RenderCallback<Item> = (item: Item, stage: StatusState) => React.ReactNode
type RenderCallback<Item> = (item: Item, stage: StatusState & { key: string | number }) => React.ReactNode

type ItemWithState<Item> = {
item: Item
Expand All @@ -18,8 +18,22 @@ interface ItemWithKey<Item> {
index: number
}

export function useListTransition<Item>(list: Array<Item>, options?: { timeout: Timeout, entered?: boolean, keyExtractor?: (item: Item) => string | number }) {
const { timeout = 300, entered = true, keyExtractor: _keyExtractor } = options || {}
interface ListTransitionOptions<Item> {
timeout: Timeout
entered?: boolean
keyExtractor?: (item: Item) => string | number
viewTransition?: (fn: () => void) => void
}

const noop = (fn: any) => fn()

export function useListTransition<Item>(list: Array<Item>, options?: ListTransitionOptions<Item>) {
const {
timeout = 300,
entered = true,
keyExtractor: _keyExtractor,
viewTransition = noop,
} = options || {}
const keyRef = useRef(0)
const hasCustomKeyExtractor = !!_keyExtractor
const keyExtractor = useMemoizedFn(_keyExtractor || (() => keyRef.current))
Expand All @@ -45,17 +59,19 @@ export function useListTransition<Item>(list: Array<Item>, options?: { timeout:

// 1 add new items into list state
if (newItemsWithIndex.length > 0) {
setListState(prevListState =>
newItemsWithIndex.reduce(
(prev, { item, index }, _i) =>
insertArray(prev, index, {
item,
key: hasCustomKeyExtractor ? keyExtractor(item) : keyRef.current++,
...getState(STATUS.from),
}),
prevListState,
),
)
viewTransition(() => {
setListState(prevListState =>
newItemsWithIndex.reduce(
(prev, { item, index }, _i) =>
insertArray(prev, index, {
item,
key: hasCustomKeyExtractor ? keyExtractor(item) : keyRef.current++,
...getState(STATUS.from),
}),
prevListState,
),
)
})
}

// 2 enter those new items immediatly
Expand Down Expand Up @@ -94,13 +110,15 @@ export function useListTransition<Item>(list: Array<Item>, options?: { timeout:
const subtractItems = subtractItemStates.map(item => item.item)

if (newItemsWithIndex.length === 0 && subtractItemStates.length > 0) {
setListState(prev =>
prev.map(itemState =>
subtractItemStates.includes(itemState)
? { ...itemState, ...getState(STATUS.exiting) }
: itemState,
),
)
viewTransition(() => {
setListState(prev =>
prev.map(itemState =>
subtractItemStates.includes(itemState)
? { ...itemState, ...getState(STATUS.exiting) }
: itemState,
),
)
})

setAnimationFrameTimeout(() => {
setListState(prev =>
Expand All @@ -114,16 +132,18 @@ export function useListTransition<Item>(list: Array<Item>, options?: { timeout:
&& list.length === listState.length
&& list.some((item, index) => keyExtractor(item) !== listState[index].key)
) {
setListState(
list.map(item => ({
item,
key: keyExtractor(item),
...getState(STATUS.entered),
})),
)
viewTransition(() => {
setListState(
list.map(item => ({
item,
key: keyExtractor(item),
...getState(STATUS.entered),
})),
)
})
}
},
[list, listState, enterTimeout, exitTimeout, entered, keyExtractor, hasCustomKeyExtractor],
[list, listState, enterTimeout, exitTimeout, entered, keyExtractor, hasCustomKeyExtractor, viewTransition],
)

function transitionList(renderCallback: RenderCallback<Item>) {
Expand Down
1 change: 1 addition & 0 deletions src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function getState(status: STATUS) {
return {
_s: status,
status: STATUS[status] as Stage,
simpleStatus: getSimpleStatus(STATUS[status] as Stage),
/**
* status !== 'exited',
*/
Expand Down
14 changes: 14 additions & 0 deletions src/viewTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { flushSync } from 'react-dom'

const isClient = typeof window !== 'undefined'
const isSupportViewTransition = isClient && 'startViewTransition' in document

export function startViewTransition(fn: () => void) {
if (isSupportViewTransition) {
// @ts-expect-error startViewTransition is not in the type definition
document.startViewTransition(() => flushSync(fn))
}
else {
fn()
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"jsx": "react-jsx",
"lib": ["esnext", "dom"],
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
Expand Down

0 comments on commit a847816

Please sign in to comment.