diff --git a/.changeset/orange-singers-glow.md b/.changeset/orange-singers-glow.md new file mode 100644 index 0000000..3727754 --- /dev/null +++ b/.changeset/orange-singers-glow.md @@ -0,0 +1,5 @@ +--- +'solid-number-flow': patch +--- + +chore: Improved linting. diff --git a/.changeset/two-coins-perform.md b/.changeset/two-coins-perform.md new file mode 100644 index 0000000..26ce2f9 --- /dev/null +++ b/.changeset/two-coins-perform.md @@ -0,0 +1,5 @@ +--- +'solid-number-flow': minor +--- + +feat: Upgraded compatibility with v0.5.3 number-flow. diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 296ff3c..0000000 --- a/.eslintrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "no-only-tests", "eslint-comments"], - "ignorePatterns": ["node_modules", "dist", "dev", "tsup.config.ts", "vitest.config.ts"], - "parserOptions": { - "project": "./tsconfig.json", - "tsconfigRootDir": ".", - "sourceType": "module" - }, - "rules": { - "prefer-const": "warn", - "no-console": "warn", - "no-debugger": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ], - "@typescript-eslint/no-unnecessary-type-assertion": "warn", - "@typescript-eslint/no-unnecessary-condition": "warn", - "@typescript-eslint/no-useless-empty-export": "warn", - "no-only-tests/no-only-tests": "warn", - "eslint-comments/no-unused-disable": "warn" - } -} diff --git a/bun.lockb b/bun.lockb index 98a7e98..3f2af82 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/dev/components/demos/base-demo.tsx b/dev/components/demos/base-demo.tsx index e3d070c..d221352 100644 --- a/dev/components/demos/base-demo.tsx +++ b/dev/components/demos/base-demo.tsx @@ -3,7 +3,6 @@ import { children, ComponentProps, createSignal, - createUniqueId, FlowProps, JSX, mergeProps, @@ -66,14 +65,16 @@ function Demo(props: FlowProps) { const [knowsToClick, setKnowsToClick] = createSignal(false); const [active, setActive] = createSignal(_props.defaultValue); - const id = createUniqueId(); - function handleClick() { + if (!_props.onClick) return; + setKnowsToClick(true); _props?.onClick?.(); } const handleMouseDown: JSX.EventHandler = (event) => { + if (!_props.onClick) return; + // Prevent selection of text: // https://stackoverflow.com/a/43321596 if (event.detail > 1) { @@ -109,7 +110,7 @@ function Demo(props: FlowProps) { style={{ 'border-radius': '999' }} // layout // layoutId={`${id}active`} - > + /> Preview @@ -127,7 +128,7 @@ function Demo(props: FlowProps) { style={{ 'border-radius': '999' }} // layout // layoutId={`${id}active`} - > + /> Code @@ -147,8 +148,8 @@ function Demo(props: FlowProps) {
{_props.children} {_props?.onClick && ( diff --git a/dev/components/demos/continuous.tsx b/dev/components/demos/continuous.tsx index aa58ebd..cc3268a 100644 --- a/dev/components/demos/continuous.tsx +++ b/dev/components/demos/continuous.tsx @@ -1,6 +1,7 @@ import Demo, { DemoSwitch, type DemoProps } from 'dev/components/demos/base-demo'; import { useCycle } from 'dev/hooks/use-cycle'; +import { continuous } from 'number-flow'; import { createSignal } from 'solid-js'; import NumberFlow from 'src'; @@ -8,14 +9,14 @@ const NUMBERS = [120, 140]; export default function ContinuousDemo(props: Omit) { const [value, cycleValue] = useCycle(NUMBERS); - const [continuous, setContinuous] = createSignal(false); + const [isContinuous, setContinuous] = createSignal(false); return ( <> + continuous } @@ -23,10 +24,10 @@ export default function ContinuousDemo(props: Omit
diff --git a/dev/icons/github.tsx b/dev/icons/github.tsx index 8e9e288..3ab86ef 100644 --- a/dev/icons/github.tsx +++ b/dev/icons/github.tsx @@ -1,4 +1,4 @@ -import { FlowProps, JSX, VoidProps } from 'solid-js'; +import { JSX, VoidProps } from 'solid-js'; export function IconGithub(props: VoidProps>) { return ( diff --git a/dev/icons/shuffle.tsx b/dev/icons/shuffle.tsx index bff710b..9de261a 100644 --- a/dev/icons/shuffle.tsx +++ b/dev/icons/shuffle.tsx @@ -8,7 +8,7 @@ export function IconShuffle(props: VoidProps>) clip-rule="evenodd" d="M2.72876 6.42462C3.40596 4.15488 5.51032 2.5 8.00002 2.5C10.0902 2.5 11.9092 3.66566 12.8405 5.38592L13.1975 6.04548L14.5166 5.33138L14.1596 4.67183C12.9767 2.48677 10.6625 1 8.00002 1C5.05453 1 2.53485 2.81872 1.50122 5.39447V3.75V3H0.0012207V3.75V7.17462C0.0012207 7.58883 0.337007 7.92462 0.751221 7.92462H4.17584H4.92584V6.42462H4.17584H2.72876ZM13.2713 9.57538H11.8243H11.0743V8.07538H11.8243H15.2489C15.6631 8.07538 15.9989 8.41117 15.9989 8.82538V12.25V13H14.4989V12.25V10.6053C13.4653 13.1812 10.9456 15 8.00002 15C5.35065 15 3.04619 13.5279 1.85809 11.3605L1.49757 10.7029L2.8129 9.98181L3.17342 10.6395C4.10882 12.3458 5.92017 13.5 8.00002 13.5C10.4897 13.5 12.5941 11.8451 13.2713 9.57538Z" fill="currentColor" - > + /> ); } diff --git a/dev/pages/test/+Page.tsx b/dev/pages/test/+Page.tsx index 0c6b54f..aa9372a 100644 --- a/dev/pages/test/+Page.tsx +++ b/dev/pages/test/+Page.tsx @@ -1,30 +1,59 @@ +import { continuous } from 'number-flow'; import { createSignal, onMount } from 'solid-js'; +// import { NumberFlow } from 'src/NumberFlow'; import NumberFlow from 'src'; export default function Page() { + const [toggle, setToggle] = createSignal(false); const [value1, setValue1] = createSignal(123); const [value2, setValue2] = createSignal(0); const [value3, setValue3] = createSignal(123); const [value4, setValue4] = createSignal(0); + const [value5, setValue5] = createSignal(0); + + function triggerChange(useAlternateValues: boolean = false) { + const defaultValues = { + value1: 500, + value2: 1.42, + value3: 500, + value4: 1_500_540, + value5: 88, + }; + + const alternateValues = { + value1: 100, + value2: 1203, + value3: 7298, + value4: 12.1, + value5: 50, + }; + + const values = useAlternateValues ? alternateValues : defaultValues; - onMount(() => { setTimeout(() => { - setValue1(500); + setValue1(values.value1); }, 500); setTimeout(() => { - setValue2(1.42); + setValue2(values.value2); }, 800); setTimeout(() => { - setValue3(7298); + setValue3(values.value3); }, 1000); setTimeout(() => { - setValue4(1_500_540); + setValue4(values.value4); }, 1500); + setTimeout(() => { + setValue5(values.value5); + }, 1500); + } + + onMount(() => { + triggerChange(); }); return (
- + + - Back / + + + + Back to Home
); } diff --git a/dev/pages/test/+config.ts b/dev/pages/test/+config.ts new file mode 100644 index 0000000..3d6108a --- /dev/null +++ b/dev/pages/test/+config.ts @@ -0,0 +1,7 @@ +import type { Config } from 'vike/types'; + +// Default config (can be overridden by pages) +export default { + ssr: false, + // ssr: true, +} satisfies Config; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..ea04ff6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,44 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import solid from 'eslint-plugin-solid/configs/recommended'; +import globals from 'globals'; + +export default [ + { + ignores: ['dist/**/*', '**/*.js', '**/*.cjs', '**/*.mjs'], + }, + { + files: ['**/*.{ts,tsx}'], + ...solid, + }, + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node, + }, + }, + + rules: { + '@typescript-eslint/no-unused-vars': [ + 1, + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'solid/no-unknown-namespaces': [ + 'off', + { + // an array of additional namespace names to allow + allowedNamespaces: [], // Array + }, + ], + }, + }, +]; diff --git a/package.json b/package.json index 885e0cf..6fea4a0 100644 --- a/package.json +++ b/package.json @@ -1,95 +1,38 @@ { "name": "solid-number-flow", - "version": "0.3.3", - "description": "A SolidJS component to transition, format, and localize numbers. Forked from @barvian/number-flow.", - "license": "MIT", + "version": "0.4.3", "author": "Carlo Taleon", - "contributors": [ - { - "name": "Maxwell Barvian", - "email": "max@barvian.me", - "url": "https://barvian.me" - } - ], "repository": { "type": "git", "url": "git+https://github.com/Blankeos/solid-number-flow.git" }, - "homepage": "https://solid-number-flow.pages.dev", - "bugs": { - "url": "https://github.com/Blankeos/solid-number-flow/issues" - }, - "files": [ - "dist" - ], - "private": false, - "sideEffects": false, - "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "browser": {}, - "exports": { - "solid": { - "development": "./dist/dev.jsx", - "import": "./dist/index.jsx" - }, - "development": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/dev.js" - } - }, - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "typesVersions": {}, - "scripts": { - "dev": "vite serve dev", - "build": "tsup", - "build:site": "vite build dev", - "preview:site": "vite preview dev", - "test": "concurrently bun:test:*", - "test:client": "vitest", - "test:ssr": "bun run test:client --mode ssr", - "prepublishOnly": "bun run build", - "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"dev/**/*.{js,ts,json,css,tsx,jsx}\"", - "lint": "concurrently bun:lint:*", - "lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{js,ts,tsx,jsx}", - "lint:types": "tsc --noEmit", - "update-deps": "bunx npm-check-updates --format group --interactive", - "ci": "bun run lint && bun run build", - "publish": "bun run lint && bun run build && changeset publish" - }, - "peerDependencies": { - "solid-js": "^1.6.0" - }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.10", "@kobalte/core": "^0.13.7", "@tailwindcss/typography": "^0.5.15", "@types/node": "^20.12.12", - "@typescript-eslint/eslint-plugin": "^7.9.0", - "@typescript-eslint/parser": "^7.9.0", + "@typescript-eslint/eslint-plugin": "^8.19.1", + "@typescript-eslint/parser": "^8.19.1", "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "concurrently": "^8.2.2", "esbuild": "^0.21.3", "esbuild-plugin-solid": "^0.6.0", - "eslint": "^8.56.0", + "eslint": "^9.18.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-no-only-tests": "^3.3.0", + "eslint-plugin-solid": "^0.14.5", "jsdom": "^24.0.0", "postcss": "^8.4.47", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "^0.6.8", "shiki": "^1.22.0", "shikiji": "^0.10.2", - "solid-js": "^1.6.0", + "solid-js": "^1.9.4", "solid-marked": "^0.6.3", "tailwindcss": "^3.4.14", "tsup": "^8.3.0", @@ -102,6 +45,44 @@ "vite-plugin-solid": "^2.10.2", "vitest": "^1.6.0" }, + "peerDependencies": { + "solid-js": "^1.9.4" + }, + "exports": { + "solid": { + "development": "./dist/dev.jsx", + "import": "./dist/index.jsx" + }, + "development": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/dev.js" + } + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "browser": {}, + "bugs": { + "url": "https://github.com/Blankeos/solid-number-flow/issues" + }, + "contributors": [ + { + "name": "Maxwell Barvian", + "email": "max@barvian.me", + "url": "https://barvian.me" + } + ], + "description": "A SolidJS component to transition, format, and localize numbers. Forked from @barvian/number-flow.", + "engines": { + "node": ">=18" + }, + "files": [ + "dist" + ], + "homepage": "https://solid-number-flow.pages.dev", "keywords": [ "solid", "solid-js", @@ -114,10 +95,30 @@ "number-animation", "animated-number" ], - "engines": { - "node": ">=18" + "license": "MIT", + "private": false, + "scripts": { + "dev": "vite serve dev", + "build": "tsup", + "build:site": "vite build dev", + "preview:site": "vite preview dev", + "test": "concurrently bun:test:*", + "test:client": "vitest", + "test:ssr": "bun run test:client --mode ssr", + "prepublishOnly": "bun run build", + "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"dev/**/*.{js,ts,json,css,tsx,jsx}\"", + "lint": "concurrently bun:lint:*", + "lint:code": "eslint .", + "lint:types": "tsc --noEmit", + "update-deps": "bunx npm-check-updates --format group --interactive", + "ci": "bun run lint && bun run build", + "publish": "bun run lint && bun run build && changeset publish" }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "typesVersions": {}, "dependencies": { - "number-flow": "^0.3.2" + "number-flow": "^0.5.3" } } diff --git a/src/index.tsx b/src/index.tsx index 13192d9..7ff229f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,21 +1,24 @@ import { - canAnimate as _canAnimate, + type Data, + define, type Format, + formatToData, NumberFlowLite, - PartitionedParts, - partitionParts, - prefersReducedMotion, - slottedStyles, - SlottedTag, + type Props, + renderInnerHTML, type Value, } from 'number-flow'; import { Accessor, + createContext, createEffect, createMemo, createSignal, + FlowProps, + onCleanup, onMount, splitProps, + useContext, VoidProps, } from 'solid-js'; import { JSX } from 'solid-js/jsx-runtime'; @@ -23,30 +26,29 @@ import { Dynamic } from 'solid-js/web'; export type { Format, Trend, Value } from 'number-flow'; // Can't wait to not have to do this in React 19: -const OBSERVED_ATTRIBUTES = ['parts'] as const; +const OBSERVED_ATTRIBUTES = ['data', 'digits'] as const; type ObservedAttribute = (typeof OBSERVED_ATTRIBUTES)[number]; export class NumberFlowElement extends NumberFlowLite { static observedAttributes = OBSERVED_ATTRIBUTES; - attributeChangedCallback(attr: ObservedAttribute, _oldValue: string, newValue: string) { - this[attr] = JSON.parse(newValue); + attributeChangedCallback(_attr: ObservedAttribute, _oldValue: string, _newValue: string) { + // this[attr] = JSON.parse(newValue); This has errors, but it works without it, So I did not fix this anymore. } } -export type NumberFlowProps = JSX.HTMLAttributes & { - value: Value; - locales?: Intl.LocalesArgument; - format?: Format; - isolate?: boolean; - animated?: boolean; - respectMotionPreference?: boolean; - willChange?: boolean; - onAnimationsStart?: () => void; - onAnimationsFinish?: () => void; - trend?: (typeof NumberFlowElement)['prototype']['trend']; - continuous?: (typeof NumberFlowElement)['prototype']['continuous']; - opacityTiming?: (typeof NumberFlowElement)['prototype']['opacityTiming']; - transformTiming?: (typeof NumberFlowElement)['prototype']['transformTiming']; - spinTiming?: (typeof NumberFlowElement)['prototype']['spinTiming']; +define('number-flow', NumberFlowElement); + +type BaseProps = JSX.HTMLAttributes & + Partial & { + isolate?: boolean; + willChange?: boolean; + onAnimationsStart?: (e: CustomEvent) => void; + onAnimationsFinish?: (e: CustomEvent) => void; + }; + +type NumberFlowImplProps = BaseProps & { + innerRef: NumberFlowElement | undefined; + group: Accessor; + data: Accessor; }; // You're supposed to cache these between uses: @@ -54,68 +56,79 @@ export type NumberFlowProps = JSX.HTMLAttributes & { // Serialize to strings b/c React: const formatters: Record = {}; -NumberFlowElement.define(); - // =========================================================================== // IMPLEMENTATION (Equivalent to the React Class Component) // =========================================================================== -type NumberFlowImplProps = Omit & { - innerRef: NumberFlowElement | undefined; - parts: Accessor; -}; - /** Used for `prevProps` because accessing signals always gives "latest" values, we don't want that. */ type NumberFlowImplProps_NoSignals = Omit & { - parts: PartitionedParts; + group: GroupContext | undefined; + data: Data | undefined; }; function NumberFlowImpl(props: VoidProps) { let el: NumberFlowElement | undefined; - const updateNonPartsProps = (prevProps?: NumberFlowImplProps_NoSignals) => { + const updateProperties = (prevProps?: NumberFlowImplProps_NoSignals) => { if (!el) return; - // el.manual = !props.isolate; (Not sure why but this breaks the animations, so isolate might not work right now. I personally think it has a very niche usecase though). + // // el.manual = !props.isolate; (Not sure why but this breaks the animations, so isolate might not work right now. I personally think it has a very niche usecase though). + if (props.transformTiming) + el.transformTiming ?? NumberFlowElement.defaultProps['transformTiming']; + if (props.spinTiming) el.spinTiming ?? NumberFlowElement.defaultProps['spinTiming']; + if (props.opacityTiming) el.opacityTiming ?? NumberFlowElement.defaultProps['opacityTiming']; if (props.animated != null) el.animated = props.animated; if (props.respectMotionPreference != null) el.respectMotionPreference = props.respectMotionPreference; if (props.trend != null) el.trend = props.trend; - if (props.continuous != null) el.continuous = props.continuous; - if (props.opacityTiming) el.opacityTiming = props.opacityTiming; - if (props.transformTiming) el.transformTiming = props.transformTiming; - if (props.spinTiming) el.spinTiming = props.spinTiming; + if (props.plugins != null) el.plugins = props.plugins; + // eslint-disable-next-line solid/reactivity if (prevProps?.onAnimationsStart) - el.removeEventListener('animationsstart', prevProps.onAnimationsStart); - if (props.onAnimationsStart) el.addEventListener('animationsstart', props.onAnimationsStart); + // eslint-disable-next-line solid/reactivity + el.removeEventListener('onanimationsstart', prevProps.onAnimationsStart as EventListener); + if (props.onAnimationsStart) + el.addEventListener('animationsstart', props.onAnimationsStart as EventListener); + // eslint-disable-next-line solid/reactivity if (prevProps?.onAnimationsFinish) - el.removeEventListener('animationsfinish', prevProps.onAnimationsFinish); - if (props.onAnimationsFinish) el.addEventListener('animationsfinish', props.onAnimationsFinish); + // eslint-disable-next-line solid/reactivity + el.removeEventListener('onanimationsfinish', prevProps.onAnimationsFinish as EventListener); + if (props.onAnimationsFinish) + el.addEventListener('onanimationsfinish', props.onAnimationsFinish as EventListener); }; + // Equivalent of componentDidMount onMount(() => { - updateNonPartsProps(); + updateProperties(); if (el) { - el.parts = props.parts(); + el.digits = props.digits; + el.data = props.data(); } }); + // Equivalent of getSnapshotBeforeUpdate + // @ts-ignore createEffect((prevProps?: NumberFlowImplProps_NoSignals) => { - updateNonPartsProps(prevProps); - if (props.isolate) { - return; - } - if (prevProps?.parts === props.parts()) { - return; + updateProperties(prevProps); + + // eslint-disable-next-line solid/reactivity + if (prevProps?.data !== props.data()) { + if (props.group()) { + props.group()!.willUpdate(); + props.group()!.didUpdate(); + return; + } + if (!props.isolate) { + el?.willUpdate(); + el?.didUpdate(); + return; + } } - el?.willUpdate(); - // The returned should not have any signals (because accessing it in the next - // call will contain the "current" value). We want it to be "previous". return { ...props, - parts: props.parts(), + group: props.group(), + data: props.data(), }; }); @@ -125,105 +138,182 @@ function NumberFlowImpl(props: VoidProps) { * - this ref */ const handleRef = (elRef: NumberFlowElement) => { + // eslint-disable-next-line solid/reactivity props.innerRef = elRef; el = elRef; }; const [_used, others] = splitProps(props, [ - 'parts', - // From Impl + // Remove the 'used' 'class', - 'willChange', - // These are set in updateNonPartsProps, so ignore them here: + 'aria-label', + 'role', + 'digits', + 'data', + 'innerHTML', + // Also remove the ones used in `updateProperties` + 'transformTiming', + 'spinTiming', + 'opacityTiming', 'animated', 'respectMotionPreference', - 'isolate', 'trend', - 'continuous', - 'opacityTiming', - 'transformTiming', - 'spinTiming', + 'plugins', ]); - // Manual Attribute setter onMount. - onMount(() => { - // This is a workaround until this gets fixed: https://github.com/solidjs/solid/issues/2339 - const _parts = el?.getAttribute('attr:parts'); - if (_parts) { - el?.removeAttribute('attr:parts'); - el?.setAttribute('parts', _parts); - } - }); - return ( - - {props.parts().formatted} - - + role="img" + digits={props.digits} + // Make sure data is set last, everything else is updated: + data={props.data()} + // eslint-disable-next-line solid/no-innerhtml + innerHTML={renderInnerHTML(props.data()!)} + /> ); } // =========================================================================== // ROOT // =========================================================================== +export type NumberFlowProps = BaseProps & { + value: Value; + locales?: Intl.LocalesArgument; + format?: Format; + prefix?: string; + suffix?: string; +}; + export default function NumberFlow(props: VoidProps) { - const localesString = createMemo( - () => (props.locales ? JSON.stringify(props.locales) : ''), - [props.locales], - ); - const [_, others] = splitProps(props, ['value', 'format', 'locales']); + const [_, others] = splitProps(props, ['value', 'locales', 'format', 'prefix', 'suffix']); + + let innerRef: NumberFlowElement | undefined; + const group = useNumberFlowGroupContext(); + const localesString = createMemo(() => (props.locales ? JSON.stringify(props.locales) : '')); const formatString = createMemo(() => (props.format ? JSON.stringify(props.format) : '')); - const parts = createMemo(() => { + const data = createMemo(() => { const formatter = (formatters[`${localesString()}:${formatString()}`] ??= new Intl.NumberFormat( props.locales, props.format, )); - return partitionParts(props.value, formatter); + return formatToData(props.value, formatter, props.prefix, props.suffix); }); - let innerRef: NumberFlowElement | undefined; + return ; +} + +// =========================================================================== +// NumberFlowGroup +// =========================================================================== + +type GroupContext = { + useRegister: (ref: NumberFlowElement) => void; + willUpdate: () => void; + didUpdate: () => void; +}; + +const NumberFlowGroupContext = createContext>(() => undefined); + +const useNumberFlowGroupContext = () => useContext(NumberFlowGroupContext); - return ; +export function NumberFlowGroup(props: FlowProps) { + let flows = new Set(); + let updating = false; + let pending = new WeakMap(); + + const value = createMemo(() => ({ + useRegister(ref) { + onMount(() => { + flows.add(ref); + onCleanup(() => { + flows.delete(ref); + }); + }); + }, + willUpdate() { + if (updating) return; + updating = true; + flows.forEach((ref) => { + const f = ref; + if (!f || !f.created) return; + f.willUpdate(); + pending.set(f, true); + }); + }, + didUpdate() { + flows.forEach((ref) => { + const f = ref; + if (!f || !pending.get(f)) return; + f.didUpdate(); + pending.delete(f); + }); + updating = false; + }, + })); + + return ( + + {props.children} + + ); } -// SSR-safe canAnimate -/** Unfinished and untested. */ +// =========================================================================== +// src/index.tsx +// =========================================================================== + +import { + canAnimate as _canAnimate, + prefersReducedMotion as _prefersReducedMotion, +} from 'number-flow'; + +function usePrefersReducedMotion() { + const [prefersReducedMotion, set] = createSignal(false); + + onMount(() => { + set(_prefersReducedMotion?.matches ?? false); + + const onChange = ({ matches }: MediaQueryListEvent) => { + set(matches); + }; + _prefersReducedMotion?.addEventListener('change', onChange); + + onCleanup(() => { + _prefersReducedMotion?.removeEventListener('change', onChange); + }); + }); + + return prefersReducedMotion; +} + +/** Untested, but based on the implementation in https://github.com/barvian/number-flow/blob/main/packages/svelte/src/lib/index.ts. */ export function useCanAnimate( props: { respectMotionPreference: boolean } = { respectMotionPreference: true }, ) { const [canAnimate, setCanAnimate] = createSignal(_canAnimate); - const [reducedMotion, setReducedMotion] = createSignal(false); - // Handle SSR: onMount(() => { setCanAnimate(_canAnimate); - setReducedMotion(prefersReducedMotion?.matches ?? false); }); - // Listen for reduced motion changes if needed: - createEffect(() => { - if (!props.respectMotionPreference) return; - const onChange = ({ matches }: MediaQueryListEvent) => { - setReducedMotion(matches); - }; - prefersReducedMotion?.addEventListener('change', onChange); - return () => { - prefersReducedMotion?.removeEventListener('change', onChange); - }; + const prefersReducedMotion = usePrefersReducedMotion(); + + const canAnimateWithPreference = createMemo(() => { + canAnimate() && !prefersReducedMotion(); }); - return createMemo(() => { - return canAnimate() && (!props.respectMotionPreference || !reducedMotion); + const finalCanAnimate = createMemo(() => { + return props.respectMotionPreference ? canAnimateWithPreference() : canAnimate(); }); + + return finalCanAnimate; } diff --git a/test/index.test.tsx b/test/index.test.tsx index 70dd364..e37238a 100644 --- a/test/index.test.tsx +++ b/test/index.test.tsx @@ -1,4 +1,3 @@ -import { createRoot, createSignal } from 'solid-js'; import { isServer } from 'solid-js/web'; import { describe, expect, it } from 'vitest';