From a5d6a8f9b4b36f892d47184115d4c45d18aa0dd3 Mon Sep 17 00:00:00 2001 From: Cahil Foley <14274957+cahilfoley@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:56:56 +0800 Subject: [PATCH] fix(ESM): Update the published library to include file extensions for proper ESM compatibility Fixes #76, #73 --- package-lock.json | 11 +- packages/react-snowfall/package.json | 2 +- packages/react-snowfall/src/Snowfall.tsx | 146 +++--- packages/react-snowfall/src/SnowfallCanvas.ts | 4 +- packages/react-snowfall/src/Snowflake.ts | 482 +++++++++--------- packages/react-snowfall/src/hooks.ts | 186 +++---- packages/react-snowfall/src/index.ts | 6 +- packages/react-snowfall/tsconfig.json | 6 +- 8 files changed, 422 insertions(+), 421 deletions(-) diff --git a/package-lock.json b/package-lock.json index fff50d3..9b20263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14512,10 +14512,11 @@ "dev": true }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15682,7 +15683,7 @@ } }, "packages/react-snowfall": { - "version": "1.2.1", + "version": "2.1.0", "license": "MIT", "dependencies": { "react-fast-compare": "^3.2.2" @@ -15702,7 +15703,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "standard-version": "^9.5.0", - "typescript": "^5.2.2" + "typescript": "^5.5.4" }, "peerDependencies": { "react": "^16.8 || 17.x || 18.x", diff --git a/packages/react-snowfall/package.json b/packages/react-snowfall/package.json index 698af0e..d6701f8 100644 --- a/packages/react-snowfall/package.json +++ b/packages/react-snowfall/package.json @@ -51,7 +51,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "standard-version": "^9.5.0", - "typescript": "^5.2.2" + "typescript": "^5.5.4" }, "dependencies": { "react-fast-compare": "^3.2.2" diff --git a/packages/react-snowfall/src/Snowfall.tsx b/packages/react-snowfall/src/Snowfall.tsx index daa6706..3105f67 100644 --- a/packages/react-snowfall/src/Snowfall.tsx +++ b/packages/react-snowfall/src/Snowfall.tsx @@ -1,73 +1,73 @@ -import React, { useEffect, useRef } from 'react' -import { SnowfallCanvas, SnowfallCanvasConfig } from './SnowfallCanvas' -import { defaultConfig } from './Snowflake' -import { useComponentSize, useDeepMemo, useSnowfallStyle } from './hooks' - -export interface SnowfallProps extends Partial { - /** - * Any style properties that will be passed to the canvas element. - */ - style?: React.CSSProperties -} - -export const Snowfall = ({ - color = defaultConfig.color, - changeFrequency = defaultConfig.changeFrequency, - radius = defaultConfig.radius, - speed = defaultConfig.speed, - wind = defaultConfig.wind, - rotationSpeed = defaultConfig.rotationSpeed, - snowflakeCount = 150, - images, - style, -}: SnowfallProps = {}): JSX.Element => { - const mergedStyle = useSnowfallStyle(style) - - const canvasRef = useRef(null) - const canvasSize = useComponentSize(canvasRef) - - const config = useDeepMemo({ - color, - changeFrequency, - radius, - speed, - wind, - rotationSpeed, - images, - snowflakeCount, - }) - - // A reference to the config used for creating the initial instance - const configRef = useRef(config) - - const snowfallCanvasRef = useRef() - - useEffect(() => { - if (!snowfallCanvasRef.current && canvasRef.current) { - snowfallCanvasRef.current = new SnowfallCanvas(canvasRef.current, configRef.current) - } - - return () => { - snowfallCanvasRef.current?.pause() - snowfallCanvasRef.current = undefined - } - }, []) - - useEffect(() => { - if (snowfallCanvasRef.current) { - snowfallCanvasRef.current.updateConfig(config) - } - }, [config]) - - return ( - - ) -} - -export default Snowfall +import React, { useEffect, useRef } from 'react' +import { SnowfallCanvas, SnowfallCanvasConfig } from './SnowfallCanvas.js' +import { defaultConfig } from './Snowflake.js' +import { useComponentSize, useDeepMemo, useSnowfallStyle } from './hooks.js' + +export interface SnowfallProps extends Partial { + /** + * Any style properties that will be passed to the canvas element. + */ + style?: React.CSSProperties +} + +export const Snowfall = ({ + color = defaultConfig.color, + changeFrequency = defaultConfig.changeFrequency, + radius = defaultConfig.radius, + speed = defaultConfig.speed, + wind = defaultConfig.wind, + rotationSpeed = defaultConfig.rotationSpeed, + snowflakeCount = 150, + images, + style, +}: SnowfallProps = {}): JSX.Element => { + const mergedStyle = useSnowfallStyle(style) + + const canvasRef = useRef(null) + const canvasSize = useComponentSize(canvasRef) + + const config = useDeepMemo({ + color, + changeFrequency, + radius, + speed, + wind, + rotationSpeed, + images, + snowflakeCount, + }) + + // A reference to the config used for creating the initial instance + const configRef = useRef(config) + + const snowfallCanvasRef = useRef() + + useEffect(() => { + if (!snowfallCanvasRef.current && canvasRef.current) { + snowfallCanvasRef.current = new SnowfallCanvas(canvasRef.current, configRef.current) + } + + return () => { + snowfallCanvasRef.current?.pause() + snowfallCanvasRef.current = undefined + } + }, []) + + useEffect(() => { + if (snowfallCanvasRef.current) { + snowfallCanvasRef.current.updateConfig(config) + } + }, [config]) + + return ( + + ) +} + +export default Snowfall diff --git a/packages/react-snowfall/src/SnowfallCanvas.ts b/packages/react-snowfall/src/SnowfallCanvas.ts index b400379..e86ed1b 100644 --- a/packages/react-snowfall/src/SnowfallCanvas.ts +++ b/packages/react-snowfall/src/SnowfallCanvas.ts @@ -1,5 +1,5 @@ -import Snowflake, { SnowflakeConfig, defaultConfig } from './Snowflake' -import { targetFrameTime } from './config' +import Snowflake, { SnowflakeConfig, defaultConfig } from './Snowflake.js' +import { targetFrameTime } from './config.js' export interface SnowfallCanvasConfig extends SnowflakeConfig { /** diff --git a/packages/react-snowfall/src/Snowflake.ts b/packages/react-snowfall/src/Snowflake.ts index 97b649d..eacde54 100644 --- a/packages/react-snowfall/src/Snowflake.ts +++ b/packages/react-snowfall/src/Snowflake.ts @@ -1,241 +1,241 @@ -import isEqual from 'react-fast-compare' -import { lerp, random, randomElement } from './utils' - -export interface SnowflakeProps { - /** The color of the snowflake, can be any valid CSS color. */ - color: string - /** - * The minimum and maximum radius of the snowflake, will be - * randomly selected within this range. - * - * The default value is `[0.5, 3.0]`. - */ - radius: [number, number] - /** - * The minimum and maximum speed of the snowflake. - * - * The speed determines how quickly the snowflake moves - * along the y axis (vertical speed). - * - * The values will be randomly selected within this range. - * - * The default value is `[1.0, 3.0]`. - */ - speed: [number, number] - /** - * The minimum and maximum wind of the snowflake. - * - * The wind determines how quickly the snowflake moves - * along the x axis (horizontal speed). - * - * The values will be randomly selected within this range. - * - * The default value is `[-0.5, 2.0]`. - */ - wind: [number, number] - /** - * The frequency in frames that the wind and speed values - * will update. - * - * The default value is 200. - */ - changeFrequency: number - /** - * An array of images that will be rendered as the snowflakes instead - * of the default circle shapes. - */ - images?: CanvasImageSource[] - /** - * The minimum and maximum rotation speed of the snowflake (in degrees of - * rotation per frame). - * - * The rotation speed determines how quickly the snowflake rotates when - * an image is being rendered. - * - * The values will be randomly selected within this range. - * - * The default value is `[-1.0, 1.0]`. - */ - rotationSpeed: [number, number] -} - -export type SnowflakeConfig = Partial - -export const defaultConfig: SnowflakeProps = { - color: '#dee4fd', - radius: [0.5, 3.0], - speed: [1.0, 3.0], - wind: [-0.5, 2.0], - changeFrequency: 200, - rotationSpeed: [-1.0, 1.0], -} - -interface SnowflakeParams { - x: number - y: number - radius: number - rotation: number - rotationSpeed: number - speed: number - wind: number - nextSpeed: number - nextWind: number - nextRotationSpeed: number -} - -/** - * An individual snowflake that will update it's location every call to `update` - * and draw itself to the canvas every call to `draw`. - */ -class Snowflake { - private static offscreenCanvases = new WeakMap>() - - /** - * A utility function to create a collection of snowflakes - * @param canvas The canvas element - * @param amount The number of snowflakes - * @param config The configuration for each snowflake - */ - static createSnowflakes(canvas: HTMLCanvasElement | null, amount: number, config: SnowflakeConfig): Snowflake[] { - if (!canvas) return [] - - const snowflakes: Snowflake[] = [] - - for (let i = 0; i < amount; i++) { - snowflakes.push(new Snowflake(canvas, config)) - } - - return snowflakes - } - - private config!: SnowflakeProps - private params: SnowflakeParams - private framesSinceLastUpdate: number - private image?: CanvasImageSource - - public constructor(canvas: HTMLCanvasElement, config: SnowflakeConfig = {}) { - // Set custom config - this.updateConfig(config) - - // Setting initial parameters - const { radius, wind, speed, rotationSpeed } = this.config - - this.params = { - x: random(0, canvas.offsetWidth), - y: random(-canvas.offsetHeight, 0), - rotation: random(0, 360), - radius: random(...radius), - speed: random(...speed), - wind: random(...wind), - rotationSpeed: random(...rotationSpeed), - nextSpeed: random(...wind), - nextWind: random(...speed), - nextRotationSpeed: random(...rotationSpeed), - } - - this.framesSinceLastUpdate = 0 - } - - private selectImage() { - if (this.config.images && this.config.images.length > 0) { - this.image = randomElement(this.config.images) - } else { - this.image = undefined - } - } - - public updateConfig(config: SnowflakeConfig): void { - const previousConfig = this.config - this.config = { ...defaultConfig, ...config } - this.config.changeFrequency = random(this.config.changeFrequency, this.config.changeFrequency * 1.5) - - // Update the radius if the config has changed, it won't gradually update on it's own - if (this.params && !isEqual(this.config.radius, previousConfig?.radius)) { - this.params.radius = random(...this.config.radius) - } - - if (!isEqual(this.config.images, previousConfig?.images)) { - this.selectImage() - } - } - - private updateTargetParams(): void { - this.params.nextSpeed = random(...this.config.speed) - this.params.nextWind = random(...this.config.wind) - if (this.image) { - this.params.nextRotationSpeed = random(...this.config.rotationSpeed) - } - } - - public update(offsetWidth: number, offsetHeight: number, framesPassed = 1): void { - const { x, y, rotation, rotationSpeed, nextRotationSpeed, wind, speed, nextWind, nextSpeed, radius } = this.params - - // Update current location, wrapping around if going off the canvas - this.params.x = (x + wind * framesPassed) % (offsetWidth + radius * 2) - if (this.params.x > offsetWidth + radius) this.params.x = -radius - this.params.y = (y + speed * framesPassed) % (offsetHeight + radius * 2) - if (this.params.y > offsetHeight + radius) this.params.y = -radius - - // Apply rotation - if (this.image) { - this.params.rotation = (rotation + rotationSpeed) % 360 - } - - // Update the wind, speed and rotation towards the desired values - this.params.speed = lerp(speed, nextSpeed, 0.01) - this.params.wind = lerp(wind, nextWind, 0.01) - this.params.rotationSpeed = lerp(rotationSpeed, nextRotationSpeed, 0.01) - - if (this.framesSinceLastUpdate++ > this.config.changeFrequency) { - this.updateTargetParams() - this.framesSinceLastUpdate = 0 - } - } - - private getImageOffscreenCanvas(image: CanvasImageSource, size: number): CanvasImageSource { - if (image instanceof HTMLImageElement && image.loading) return image - let sizes = Snowflake.offscreenCanvases.get(image) - - if (!sizes) { - sizes = {} - Snowflake.offscreenCanvases.set(image, sizes) - } - - if (!(size in sizes)) { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - canvas.getContext('2d')?.drawImage(image, 0, 0, size, size) - sizes[size] = canvas - } - - return sizes[size] ?? image - } - - public draw(ctx: CanvasRenderingContext2D): void { - const { x, y, rotation, radius } = this.params - - if (this.image) { - const radian = (rotation * Math.PI) / 180 - const cos = Math.cos(radian) - const sin = Math.sin(radian) - - // Translate to the location that we will be drawing the snowflake, including any rotation that needs to be applied - // The arguments for setTransform are: a, b, c, d, e, f - // a (scaleX), b (skewY), c (skewX), d (scaleY), e (translateX), f (translateY) - ctx.setTransform(cos, sin, -sin, cos, x, y) - - // Draw the image with the center of the image at the center of the current location - const image = this.getImageOffscreenCanvas(this.image, radius) - ctx.drawImage(image, -(radius / 2), -(radius / 2), radius, radius) - } else { - // Not using images so no need to use transforms, just draw an arc in the right location - ctx.beginPath() - ctx.arc(x, y, radius, 0, 2 * Math.PI) - ctx.fillStyle = this.config.color - ctx.fill() - } - } -} - -export default Snowflake +import isEqual from 'react-fast-compare' +import { lerp, random, randomElement } from './utils.js' + +export interface SnowflakeProps { + /** The color of the snowflake, can be any valid CSS color. */ + color: string + /** + * The minimum and maximum radius of the snowflake, will be + * randomly selected within this range. + * + * The default value is `[0.5, 3.0]`. + */ + radius: [number, number] + /** + * The minimum and maximum speed of the snowflake. + * + * The speed determines how quickly the snowflake moves + * along the y axis (vertical speed). + * + * The values will be randomly selected within this range. + * + * The default value is `[1.0, 3.0]`. + */ + speed: [number, number] + /** + * The minimum and maximum wind of the snowflake. + * + * The wind determines how quickly the snowflake moves + * along the x axis (horizontal speed). + * + * The values will be randomly selected within this range. + * + * The default value is `[-0.5, 2.0]`. + */ + wind: [number, number] + /** + * The frequency in frames that the wind and speed values + * will update. + * + * The default value is 200. + */ + changeFrequency: number + /** + * An array of images that will be rendered as the snowflakes instead + * of the default circle shapes. + */ + images?: CanvasImageSource[] + /** + * The minimum and maximum rotation speed of the snowflake (in degrees of + * rotation per frame). + * + * The rotation speed determines how quickly the snowflake rotates when + * an image is being rendered. + * + * The values will be randomly selected within this range. + * + * The default value is `[-1.0, 1.0]`. + */ + rotationSpeed: [number, number] +} + +export type SnowflakeConfig = Partial + +export const defaultConfig: SnowflakeProps = { + color: '#dee4fd', + radius: [0.5, 3.0], + speed: [1.0, 3.0], + wind: [-0.5, 2.0], + changeFrequency: 200, + rotationSpeed: [-1.0, 1.0], +} + +interface SnowflakeParams { + x: number + y: number + radius: number + rotation: number + rotationSpeed: number + speed: number + wind: number + nextSpeed: number + nextWind: number + nextRotationSpeed: number +} + +/** + * An individual snowflake that will update it's location every call to `update` + * and draw itself to the canvas every call to `draw`. + */ +class Snowflake { + private static offscreenCanvases = new WeakMap>() + + /** + * A utility function to create a collection of snowflakes + * @param canvas The canvas element + * @param amount The number of snowflakes + * @param config The configuration for each snowflake + */ + static createSnowflakes(canvas: HTMLCanvasElement | null, amount: number, config: SnowflakeConfig): Snowflake[] { + if (!canvas) return [] + + const snowflakes: Snowflake[] = [] + + for (let i = 0; i < amount; i++) { + snowflakes.push(new Snowflake(canvas, config)) + } + + return snowflakes + } + + private config!: SnowflakeProps + private params: SnowflakeParams + private framesSinceLastUpdate: number + private image?: CanvasImageSource + + public constructor(canvas: HTMLCanvasElement, config: SnowflakeConfig = {}) { + // Set custom config + this.updateConfig(config) + + // Setting initial parameters + const { radius, wind, speed, rotationSpeed } = this.config + + this.params = { + x: random(0, canvas.offsetWidth), + y: random(-canvas.offsetHeight, 0), + rotation: random(0, 360), + radius: random(...radius), + speed: random(...speed), + wind: random(...wind), + rotationSpeed: random(...rotationSpeed), + nextSpeed: random(...wind), + nextWind: random(...speed), + nextRotationSpeed: random(...rotationSpeed), + } + + this.framesSinceLastUpdate = 0 + } + + private selectImage() { + if (this.config.images && this.config.images.length > 0) { + this.image = randomElement(this.config.images) + } else { + this.image = undefined + } + } + + public updateConfig(config: SnowflakeConfig): void { + const previousConfig = this.config + this.config = { ...defaultConfig, ...config } + this.config.changeFrequency = random(this.config.changeFrequency, this.config.changeFrequency * 1.5) + + // Update the radius if the config has changed, it won't gradually update on it's own + if (this.params && !isEqual(this.config.radius, previousConfig?.radius)) { + this.params.radius = random(...this.config.radius) + } + + if (!isEqual(this.config.images, previousConfig?.images)) { + this.selectImage() + } + } + + private updateTargetParams(): void { + this.params.nextSpeed = random(...this.config.speed) + this.params.nextWind = random(...this.config.wind) + if (this.image) { + this.params.nextRotationSpeed = random(...this.config.rotationSpeed) + } + } + + public update(offsetWidth: number, offsetHeight: number, framesPassed = 1): void { + const { x, y, rotation, rotationSpeed, nextRotationSpeed, wind, speed, nextWind, nextSpeed, radius } = this.params + + // Update current location, wrapping around if going off the canvas + this.params.x = (x + wind * framesPassed) % (offsetWidth + radius * 2) + if (this.params.x > offsetWidth + radius) this.params.x = -radius + this.params.y = (y + speed * framesPassed) % (offsetHeight + radius * 2) + if (this.params.y > offsetHeight + radius) this.params.y = -radius + + // Apply rotation + if (this.image) { + this.params.rotation = (rotation + rotationSpeed) % 360 + } + + // Update the wind, speed and rotation towards the desired values + this.params.speed = lerp(speed, nextSpeed, 0.01) + this.params.wind = lerp(wind, nextWind, 0.01) + this.params.rotationSpeed = lerp(rotationSpeed, nextRotationSpeed, 0.01) + + if (this.framesSinceLastUpdate++ > this.config.changeFrequency) { + this.updateTargetParams() + this.framesSinceLastUpdate = 0 + } + } + + private getImageOffscreenCanvas(image: CanvasImageSource, size: number): CanvasImageSource { + if (image instanceof HTMLImageElement && image.loading) return image + let sizes = Snowflake.offscreenCanvases.get(image) + + if (!sizes) { + sizes = {} + Snowflake.offscreenCanvases.set(image, sizes) + } + + if (!(size in sizes)) { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + canvas.getContext('2d')?.drawImage(image, 0, 0, size, size) + sizes[size] = canvas + } + + return sizes[size] ?? image + } + + public draw(ctx: CanvasRenderingContext2D): void { + const { x, y, rotation, radius } = this.params + + if (this.image) { + const radian = (rotation * Math.PI) / 180 + const cos = Math.cos(radian) + const sin = Math.sin(radian) + + // Translate to the location that we will be drawing the snowflake, including any rotation that needs to be applied + // The arguments for setTransform are: a, b, c, d, e, f + // a (scaleX), b (skewY), c (skewX), d (scaleY), e (translateX), f (translateY) + ctx.setTransform(cos, sin, -sin, cos, x, y) + + // Draw the image with the center of the image at the center of the current location + const image = this.getImageOffscreenCanvas(this.image, radius) + ctx.drawImage(image, -(radius / 2), -(radius / 2), radius, radius) + } else { + // Not using images so no need to use transforms, just draw an arc in the right location + ctx.beginPath() + ctx.arc(x, y, radius, 0, 2 * Math.PI) + ctx.fillStyle = this.config.color + ctx.fill() + } + } +} + +export default Snowflake diff --git a/packages/react-snowfall/src/hooks.ts b/packages/react-snowfall/src/hooks.ts index 0904c13..02df153 100644 --- a/packages/react-snowfall/src/hooks.ts +++ b/packages/react-snowfall/src/hooks.ts @@ -1,93 +1,93 @@ -import { useCallback, useLayoutEffect, useEffect, useRef, useState, useMemo } from 'react' -import isEqual from 'react-fast-compare' -import { snowfallBaseStyle } from './config' -import { getSize } from './utils' - -/** - * Returns the height and width of a HTML element, uses the `ResizeObserver` api if available to detect changes to the - * size. Falls back to listening for resize events on the window. - * @param ref A ref to the HTML element to be measured - */ -export const useComponentSize = (ref: React.RefObject) => { - const [size, setSize] = useState(getSize(ref.current)) - - const resizeHandler = useCallback(() => { - if (ref.current) { - setSize(getSize(ref.current)) - } - }, [ref]) - - useEffect(() => { - const { ResizeObserver } = window - - if (!ref.current) return - resizeHandler() - - if (typeof ResizeObserver === 'function') { - const resizeObserver = new ResizeObserver(resizeHandler) - resizeObserver.observe(ref.current) - - return () => resizeObserver.disconnect() - } else { - window.addEventListener('resize', resizeHandler) - - return () => window.removeEventListener('resize', resizeHandler) - } - }, [ref, resizeHandler]) - - return size -} - -/** - * Utility hook that merges any provided styles with the default styles - * @param overrides The style prop passed into the component - */ -export const useSnowfallStyle = (overrides?: React.CSSProperties): React.CSSProperties => { - const styles = useMemo( - () => ({ - ...snowfallBaseStyle, - ...(overrides || {}), - }), - [overrides], - ) - - return styles -} - -/** - * Same as `React.useEffect` but uses a deep comparison on the dependency array. This should only - * be used when working with non-primitive dependencies. - * - * @param effect Effect callback to run - * @param deps Effect dependencies - */ -export function useDeepCompareEffect(effect: React.EffectCallback, deps: React.DependencyList) { - const ref = useRef(deps) - - // Only update the current dependencies if they are not deep equal - if (!isEqual(deps, ref.current)) { - ref.current = deps - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - return useEffect(effect, ref.current) -} - -/** - * Utility hook to stabilize a reference to a value, the returned value will always match the input value - * but (unlike an inline object) will maintain [SameValueZero](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * equality until a change is made. - * - * @example - * - * const obj = useDeepMemo({ foo: 'bar', bar: 'baz' }) // <- inline object creation - * const prevValue = usePrevious(obj) // <- value from the previous render - * console.log(obj === prevValue) // <- always logs true until value changes - */ -export function useDeepMemo(value: T): T { - const [state, setState] = useState(value) - - useDeepCompareEffect(() => setState(value), [value]) - - return state -} +import { useCallback, useEffect, useRef, useState, useMemo } from 'react' +import isEqual from 'react-fast-compare' +import { snowfallBaseStyle } from './config.js' +import { getSize } from './utils.js' + +/** + * Returns the height and width of a HTML element, uses the `ResizeObserver` api if available to detect changes to the + * size. Falls back to listening for resize events on the window. + * @param ref A ref to the HTML element to be measured + */ +export const useComponentSize = (ref: React.RefObject) => { + const [size, setSize] = useState(getSize(ref.current)) + + const resizeHandler = useCallback(() => { + if (ref.current) { + setSize(getSize(ref.current)) + } + }, [ref]) + + useEffect(() => { + const { ResizeObserver } = window + + if (!ref.current) return + resizeHandler() + + if (typeof ResizeObserver === 'function') { + const resizeObserver = new ResizeObserver(resizeHandler) + resizeObserver.observe(ref.current) + + return () => resizeObserver.disconnect() + } else { + window.addEventListener('resize', resizeHandler) + + return () => window.removeEventListener('resize', resizeHandler) + } + }, [ref, resizeHandler]) + + return size +} + +/** + * Utility hook that merges any provided styles with the default styles + * @param overrides The style prop passed into the component + */ +export const useSnowfallStyle = (overrides?: React.CSSProperties): React.CSSProperties => { + const styles = useMemo( + () => ({ + ...snowfallBaseStyle, + ...(overrides || {}), + }), + [overrides], + ) + + return styles +} + +/** + * Same as `React.useEffect` but uses a deep comparison on the dependency array. This should only + * be used when working with non-primitive dependencies. + * + * @param effect Effect callback to run + * @param deps Effect dependencies + */ +export function useDeepCompareEffect(effect: React.EffectCallback, deps: React.DependencyList) { + const ref = useRef(deps) + + // Only update the current dependencies if they are not deep equal + if (!isEqual(deps, ref.current)) { + ref.current = deps + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useEffect(effect, ref.current) +} + +/** + * Utility hook to stabilize a reference to a value, the returned value will always match the input value + * but (unlike an inline object) will maintain [SameValueZero](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * equality until a change is made. + * + * @example + * + * const obj = useDeepMemo({ foo: 'bar', bar: 'baz' }) // <- inline object creation + * const prevValue = usePrevious(obj) // <- value from the previous render + * console.log(obj === prevValue) // <- always logs true until value changes + */ +export function useDeepMemo(value: T): T { + const [state, setState] = useState(value) + + useDeepCompareEffect(() => setState(value), [value]) + + return state +} diff --git a/packages/react-snowfall/src/index.ts b/packages/react-snowfall/src/index.ts index 043bebf..2071a89 100644 --- a/packages/react-snowfall/src/index.ts +++ b/packages/react-snowfall/src/index.ts @@ -1,3 +1,3 @@ -export * from './SnowfallCanvas' -export * from './Snowfall' -export { default } from './Snowfall' +export * from './SnowfallCanvas.js' +export * from './Snowfall.js' +export { default } from './Snowfall.js' diff --git a/packages/react-snowfall/tsconfig.json b/packages/react-snowfall/tsconfig.json index 3f77226..83921af 100644 --- a/packages/react-snowfall/tsconfig.json +++ b/packages/react-snowfall/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2019", "moduleResolution": "node", "declaration": true, "module": "esnext", @@ -10,7 +10,7 @@ "lib": ["dom", "es2015"], "outDir": "lib", "baseUrl": "src", - "sourceMap": true + "sourceMap": true, }, - "include": ["src"] + "include": ["src"], }