Created
May 11, 2019 16:49
-
-
Save jonastreub/805b3f9f8a980c701d94bbf7c2aa39a9 to your computer and use it in GitHub Desktop.
Allow div elements to transition relative position changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useRef, useLayoutEffect, RefObject } from "react" | |
interface PositionTransitionOptions extends Partial<TransitionOptions> { | |
/** A list of {@link Dependencies} for the position of the element. This could for example be the index in a list, the window size or a combination of those. When the dependencies change a transition can happen. */ | |
dependencies: Dependencies | |
/** The element ref can be passed in if you don't want to use the ref returned by the hook. */ | |
ref?: RefObject<HTMLDivElement> | |
} | |
interface PositionInfo { | |
lastPosition: Point | |
originalTransition: string | |
} | |
/** | |
* The usePositionTransition hook allows div elements to transition changes in position. | |
* @param options - An {@link PositionTransitionOptions} object exposing all position transition features. A list of dependencies for the size of the element is required. | |
* @returns A ref for a div element. | |
*/ | |
export function usePositionTransition(options: PositionTransitionOptions): RefObject<HTMLDivElement> { | |
const ref = useRef<HTMLDivElement>(null) | |
const activeRef = options.ref ? options.ref : ref | |
const pointInfo = useRef<PositionInfo | null>(null) | |
useLayoutEffect(() => { | |
const element = activeRef.current | |
if (!element) return | |
const positionInfo = pointInfo.current | |
if (!positionInfo) { | |
pointInfo.current = { | |
lastPosition: elementOffset(element), | |
originalTransition: element.style.transition, | |
} | |
return | |
} | |
resetOffset(element) | |
const newPoint = elementOffset(element) | |
const { lastPosition } = positionInfo | |
const xDidChange = newPoint.x !== lastPosition.x | |
const yDidChange = newPoint.y !== lastPosition.y | |
const willAnimate = xDidChange || yDidChange | |
if (!willAnimate) return | |
positionInfo.lastPosition = newPoint | |
const d = Point.delta(newPoint, lastPosition) | |
element.style.transition = "" | |
element.style.transform = `translateX(${d.x}px) translateY(${d.y}px)` | |
// force styles to be applied | |
element.getBoundingClientRect() | |
element.style.transition = transitionValue("transform", options) | |
element.style.transform = "" | |
function resetOffsetHandler() { | |
if (element && positionInfo) { | |
resetOffset(element, positionInfo.originalTransition) | |
} | |
} | |
const duration = transitionDuration(options) | |
const resetDelay = transitionDurationToMilliseconds(duration) + 50 | |
const handle = setTimeout(resetOffsetHandler, resetDelay) | |
return () => { | |
clearTimeout(handle) | |
resetOffsetHandler() | |
} | |
}, toDependencyArray(options.dependencies)) | |
return activeRef | |
} | |
function elementOffset(element: HTMLDivElement): Point { | |
return { | |
x: element.offsetLeft, | |
y: element.offsetTop, | |
} | |
} | |
function resetOffset(element: HTMLDivElement, originalTransition?: string) { | |
element.style.transition = originalTransition || "" | |
element.style.transform = "" | |
} | |
interface TransitionOptions { | |
/** The duration of the transition in CSS format. The value can be in seconds or millisecond. Defaults to 0.2s. */ | |
duration: CSSProperties["transitionDuration"] | |
/** The transition timing function in CSS format. Defaults to cubic-bezier(0.2, 0, 0, 1). */ | |
timingFunction: CSSProperties["transitionTimingFunction"] | |
} | |
export function transitionDuration(options: Partial<TransitionOptions>): string { | |
return options.duration || "0.2s" | |
} | |
export function transitionTimingFunction(options: Partial<TransitionOptions>): string { | |
return options.timingFunction || "cubic-bezier(0.2, 0, 0, 1)" | |
} | |
/** Generate a CSS transition value from CSS properties and {@link TransitionOptions}. */ | |
function transitionValue( | |
properties: keyof CSSProperties | (keyof CSSProperties)[], | |
options: Partial<TransitionOptions> | |
): string { | |
const duration = transitionDuration(options) | |
const timingFunction = transitionTimingFunction(options) | |
const transitionProperties = Array.isArray(properties) ? properties : [properties] | |
return transitionProperties.map(prop => `${prop} ${duration} ${timingFunction}`).join(", ") | |
} | |
/** Convert duration from CSS format to milliseconds. */ | |
function transitionDurationToMilliseconds(duration: CSSProperties["transitionDuration"]): number { | |
if (!duration) return 0 | |
const trimmedValue = duration.trim() | |
const multiplier = trimmedValue.endsWith("ms") ? 1 : 1000 | |
const value = parseFloat(duration) | |
if (isNaN(value)) return 0 | |
return value * multiplier | |
} | |
type DependencyValue = number | string | boolean | |
/** An list of number, string, or boolean values that a hook depends on. */ | |
type Dependencies = DependencyValue | DependencyValue[] | |
/** Generate a unique key from {@link Dependencies}. */ | |
function keyFromDependencies(dependencies: Dependencies): string { | |
if (Array.isArray(dependencies)) { | |
return dependencies.map(toString).join("-") | |
} else { | |
return toString(dependencies) | |
} | |
} | |
function toDependencyArray(dependencies: Dependencies): Readonly<DependencyValue[]> { | |
if (Array.isArray(dependencies)) { | |
return dependencies | |
} else { | |
return [dependencies] | |
} | |
} | |
function toString(value: number | string | boolean): string { | |
if (typeof value === "string") return value | |
if (typeof value === "number") return value.toString() | |
if (value === true || value === false) return `${value}` | |
return "unknown" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment