Skip to content

Instantly share code, notes, and snippets.

@jniac
Created November 24, 2021 09:05
Show Gist options
  • Save jniac/4112b44d521c55535fe2293c810ff9c9 to your computer and use it in GitHub Desktop.
Save jniac/4112b44d521c55535fe2293c810ff9c9 to your computer and use it in GitHub Desktop.
React TSX Gist
import React from 'react'
import * as Animation from 'utils/Animation'
import * as MathUtils from 'math/utils'
import { Observable } from 'utils/Observable'
import { useComplexEffects } from 'utils/react'
import './Switch.css'
export const SimpleSwitch: React.FC<{
index?: number
paths?: React.ElementType[]
duration?: number
}> = ({
index = 0,
paths = [],
duration = 0.8,
}) => {
const ref1 = React.useRef<HTMLDivElement>(null)
const ref2 = React.useRef<HTMLDivElement>(null)
const indexObs = React.useMemo(() => new Observable(-1), [])
const inverseObs = React.useMemo(() => new Observable(false), [])
indexObs.setValue(index)
const hasChanged = indexObs.hasChanged && indexObs.valueOld !== -1
if (hasChanged) {
inverseObs.setValue(!inverseObs.value)
}
const [transition, setTransition] = React.useState(false)
useComplexEffects(function* () {
yield indexObs.onChange(() => {
setTransition(true)
const inverse = inverseObs.value
const [entering, leaving] = inverse ? [ref1, ref2] : [ref2, ref1]
entering.current?.classList.add('entering')
leaving.current?.classList.add('leaving')
Animation.duringWithTarget(indexObs, { duration, immediate: true }, ({ progress: t }) => {
if (leaving.current) {
const t1 = MathUtils.inverseLerp(0, 0.6, t)
leaving.current.style.opacity = Animation.easing.in4((1 - t1)).toFixed(2)
}
if (entering.current) {
entering.current.style.opacity = MathUtils.inout(t, 3, .3).toFixed(2)
}
}).onComplete(() => {
entering.current?.classList.remove('entering')
setTransition(false)
})
})
}, [])
// NOTE:
// When inverse === true, Content2 is entering / displayed, otherwise it's Content1
const inverse = inverseObs.value
const render1 = !inverse || transition
const render2 = inverse || transition
const index1 = !inverse ? indexObs.value : indexObs.valueOld
const index2 = inverse ? indexObs.value : indexObs.valueOld
const Content1 = paths[index1]
const Content2 = paths[index2]
return (
<>
{(render1 && Content1) && (
<div
ref={ref1}
className="Switch Path"
style={{ opacity: `${!inverse && transition ? 0 : 1}` }}
>
<Content1 />
</div>
)}
{(render2 && Content2) && (
<div
ref={ref2}
className="Switch Path"
style={{ opacity: `${inverse && transition ? 0 : 1}` }}
>
<Content2 />
</div>
)}
</>
)
}
import React from 'react'
import * as Animation from 'utils/Animation'
import * as MathUtils from 'math/utils'
import { Observable } from 'utils/Observable'
import { useComplexEffects } from 'utils/react'
import './DivSwitch.css'
export interface SwitchChildProps {
entering?: boolean
}
export interface SwitchProps<T> {
index?: number
paths?: React.ElementType[]
duration?: number
onTransition?: (entering: T|null, leaving: T|null, progress: number) => void
}
export const GenericSwitch = <T extends unknown>({
index = 0,
paths = [],
duration = 0.8,
onTransition,
}: SwitchProps<T>) => {
const ref1 = React.useRef<T>(null)
const ref2 = React.useRef<T>(null)
const indexObs = React.useMemo(() => new Observable(-1), [])
const inverseObs = React.useMemo(() => new Observable(false), [])
indexObs.setValue(index)
const hasChanged = indexObs.hasChanged && indexObs.valueOld !== -1
if (hasChanged) {
inverseObs.setValue(!inverseObs.value)
}
const [transition, setTransition] = React.useState(false)
useComplexEffects(function* () {
yield indexObs.onChange(() => {
setTransition(true)
const inverse = inverseObs.value
const [entering, leaving] = inverse ? [ref1, ref2] : [ref2, ref1]
Animation.duringWithTarget(indexObs, { duration, immediate: true }, ({ progress }) => {
onTransition?.(entering.current, leaving.current, progress)
}).onComplete(() => {
setTransition(false)
})
})
}, [])
// NOTE:
// When inverse === true, Content2 is entering / displayed, otherwise it's Content1
const inverse = inverseObs.value
const render1 = !inverse || transition
const render2 = inverse || transition
const index1 = !inverse ? indexObs.value : indexObs.valueOld
const index2 = inverse ? indexObs.value : indexObs.valueOld
const Content1 = paths[index1]
const Content2 = paths[index2]
return (
<>
{(render1 && Content1) && (
<Content1 ref={ref1} entering={!inverse && transition}/>
)}
{(render2 && Content2) && (
<Content2 ref={ref2} entering={inverse && transition}/>
)}
</>
)
}
export const DivSwitch: React.FC<{
index?: number
paths?: React.ElementType[]
duration?: number
}> = ({
index = 0,
paths = [],
duration = 0.8,
}) => {
return (
<GenericSwitch<HTMLDivElement>
index={index}
duration={duration}
paths={paths.map(Item => React.forwardRef<HTMLDivElement, SwitchChildProps>(({ entering }, ref) => (
<div ref={ref} className="DivSwitch Path" style={{ opacity: `${entering ? 0 : 1}` }}>
<Item />
</div>
)))}
onTransition={(entering, leaving, t) => {
if (t < 1) {
entering?.classList.add('entering')
leaving?.classList.add('leaving')
}
if (t === 1) {
entering?.classList.remove('entering')
}
if (leaving) {
const t1 = MathUtils.inverseLerp(0, 0.6, t)
leaving.style.opacity = Animation.easing.in4((1 - t1)).toFixed(2)
}
if (entering) {
entering.style.opacity = MathUtils.inout(t, 3, .3).toFixed(2)
}
}}
/>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment