Skip to content

Commit

Permalink
Merge branch 'vue'
Browse files Browse the repository at this point in the history
  • Loading branch information
barvian committed Oct 26, 2024
2 parents aabc973 + 86a6a5f commit bced8ee
Show file tree
Hide file tree
Showing 95 changed files with 7,669 additions and 646 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-jeans-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'number-flow': patch
---

automatically disable animation when hidden (see #9)
2 changes: 1 addition & 1 deletion packages/react/test/util.ts → lib/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const config = defineConfig({

/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
command: 'pnpm dev',
url: 'http://localhost:3039',
cwd: '.',
reuseExistingServer: !process.env.CI
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"type": "module",
"devDependencies": {
"@changesets/cli": "^2.27.9",
"@playwright/test": "^1.48.0",
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
"playwright": "^1.48.0",
"prettier": "^3.3.3",
Expand Down
127 changes: 80 additions & 47 deletions packages/number-flow/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createElement, offset, type HTMLProps, type Justify } from './util/dom'
import { createElement, offset, visible, type HTMLProps, type Justify } from './util/dom'
import { forEach } from './util/iterable'
import {
type KeyedDigitPart,
Expand All @@ -21,7 +21,8 @@ import styles, {
import { BROWSER } from './util/env'
import { max } from './util/math'

export { SlottedTag, slottedStyles, prefersReducedMotion } from './styles'
export { prefersReducedMotion } from './styles'
export { render, type RenderProps } from './ssr'
export * from './formatter'

export const canAnimate = supportsMod && supportsLinear && supportsAtProperty
Expand All @@ -35,46 +36,90 @@ enum Trend {
NONE = 0
}

export const defaultOpacityTiming: EffectTiming = { duration: 450, easing: 'ease-out' }
let styleSheet: CSSStyleSheet | undefined

export const defaultTransformTiming: EffectTiming = {
duration: 900,
// Make sure to keep this minified:
easing: `linear(0,.005,.019,.039,.066,.096,.129,.165,.202,.24,.278,.316,.354,.39,.426,.461,.494,.526,.557,.586,.614,.64,.665,.689,.711,.731,.751,.769,.786,.802,.817,.831,.844,.856,.867,.877,.887,.896,.904,.912,.919,.925,.931,.937,.942,.947,.951,.955,.959,.962,.965,.968,.971,.973,.976,.978,.98,.981,.983,.984,.986,.987,.988,.989,.99,.991,.992,.992,.993,.994,.994,.995,.995,.996,.996,.9963,.9967,.9969,.9972,.9975,.9977,.9979,.9981,.9982,.9984,.9985,.9987,.9988,.9989,1)`
export interface Props {
transformTiming: EffectTiming
spinTiming: EffectTiming | undefined
opacityTiming: EffectTiming
animated: boolean
manual: boolean
respectMotionPreference: boolean
trend: RawTrend
continuous: boolean
}

let styleSheet: CSSStyleSheet | undefined

// This one is used internally for framework wrappers, and
// doesn't include things like attribute support:
export class NumberFlowLite extends ServerSafeHTMLElement {
export class NumberFlowLite extends ServerSafeHTMLElement implements Props {
static defaultProps: Props = {
transformTiming: {
duration: 900,
// Make sure to keep this minified:
easing: `linear(0,.005,.019,.039,.066,.096,.129,.165,.202,.24,.278,.316,.354,.39,.426,.461,.494,.526,.557,.586,.614,.64,.665,.689,.711,.731,.751,.769,.786,.802,.817,.831,.844,.856,.867,.877,.887,.896,.904,.912,.919,.925,.931,.937,.942,.947,.951,.955,.959,.962,.965,.968,.971,.973,.976,.978,.98,.981,.983,.984,.986,.987,.988,.989,.99,.991,.992,.992,.993,.994,.994,.995,.995,.996,.996,.9963,.9967,.9969,.9972,.9975,.9977,.9979,.9981,.9982,.9984,.9985,.9987,.9988,.9989,1)`
},
spinTiming: undefined,
opacityTiming: { duration: 450, easing: 'ease-out' },
animated: true,
manual: false,
trend: true,
continuous: false,
respectMotionPreference: true
}

static define() {
if (BROWSER) customElements.define('number-flow', this)
if (!BROWSER) return
const RegisteredElement = customElements.get('number-flow')
if (
RegisteredElement &&
!(RegisteredElement === this || RegisteredElement.prototype instanceof this)
) {
console.error('An element has already been defined under the name `number-flow`.')
} else if (!RegisteredElement) {
customElements.define('number-flow', this)
}
}

transformTiming = defaultTransformTiming
spinTiming?: EffectTiming
opacityTiming = defaultOpacityTiming
#animated = true
manual = false
respectMotionPreference = true
// Kinda gross but can't do e.g. Object.assign in constructor because TypeScript
// can't determine if they're definitively assigned that way:
transformTiming = (this.constructor as typeof NumberFlowLite).defaultProps.transformTiming
spinTiming = (this.constructor as typeof NumberFlowLite).defaultProps.spinTiming
opacityTiming = (this.constructor as typeof NumberFlowLite).defaultProps.opacityTiming
manual = (this.constructor as typeof NumberFlowLite).defaultProps.manual
respectMotionPreference = (this.constructor as typeof NumberFlowLite).defaultProps
.respectMotionPreference
trend = (this.constructor as typeof NumberFlowLite).defaultProps.trend
continuous = (this.constructor as typeof NumberFlowLite).defaultProps.continuous

#animated = (this.constructor as typeof NumberFlowLite).defaultProps.animated
get animated() {
return this.#animated
}
set animated(val: boolean) {
if (this.animated === val) return
this.#animated = val
// Finish any in-flight animations (instead of cancel, which won't trigger their finish events):
this.shadowRoot?.getAnimations().forEach((a) => a.finish())
}

#created = false
#pre?: SymbolSection
#num?: Num
#post?: SymbolSection

trend: RawTrend = true
#computedTrend?: Trend
continuous = false
get computedTrend() {
return this.#computedTrend
}

#startingPlace?: number | null

get startingPlace() {
return this.#startingPlace
}
get computedTrend() {
return this.#computedTrend

#computedAnimated = this.#animated
get computedAnimated() {
return this.#computedAnimated
}

#parts?: PartitionedParts
Expand All @@ -90,7 +135,6 @@ export class NumberFlowLite extends ServerSafeHTMLElement {
if (!this.#created) {
this.#parts = parts

// Don't check for declarative shadow DOM because we'll recreate it anyway:
this.attachShadow({ mode: 'open' })

// Add stylesheet
Expand Down Expand Up @@ -160,6 +204,13 @@ export class NumberFlowLite extends ServerSafeHTMLElement {
this.#startingPlace = max(firstChangedPrev?.place, firstChanged?.place)
}

this.#computedAnimated =
canAnimate &&
this.#animated &&
(!this.respectMotionPreference || !prefersReducedMotion?.matches) &&
// https://github.com/barvian/number-flow/issues/9
visible(this)

if (!this.manual) this.willUpdate()

this.#pre!.update(pre)
Expand All @@ -173,11 +224,7 @@ export class NumberFlowLite extends ServerSafeHTMLElement {
}

willUpdate() {
// In general this should be disable most animations except the ones that run
// during an update, i.e. new digits and AnimatePresence. So make sure to handle those
// separately:
if (!this.animated) return

// Not super safe to check animated here, b/c the prop may not have been updated yet:
this.#pre!.willUpdate()
this.#num!.willUpdate()
this.#post!.willUpdate()
Expand All @@ -186,7 +233,8 @@ export class NumberFlowLite extends ServerSafeHTMLElement {
#abortAnimationsFinish?: AbortController

didUpdate() {
if (!this.animated) return
// Safe to call this here because we know the animated prop is up-to-date
if (!this.#computedAnimated) return

// If we're already animating, cancel the previous animationsfinish event:
if (this.#abortAnimationsFinish) this.#abortAnimationsFinish.abort()
Expand All @@ -206,21 +254,6 @@ export class NumberFlowLite extends ServerSafeHTMLElement {
})
this.#abortAnimationsFinish = controller
}

get animated() {
return (
canAnimate &&
this.#animated &&
(!this.respectMotionPreference || !prefersReducedMotion?.matches)
)
}

set animated(val: boolean) {
if (this.animated === val) return
this.#animated = val
// Finish any in-flight animations (instead of cancel, which won't trigger their finish events):
this.shadowRoot?.getAnimations().forEach((a) => a.finish())
}
}

class Num {
Expand Down Expand Up @@ -402,7 +435,7 @@ abstract class Section {
{ reverse }
)

if (this.flow.animated) {
if (this.flow.computedAnimated) {
const rect = this.el.getBoundingClientRect() // this should only cause a layout if there were added children
added.forEach((comp) => {
comp.willUpdate(rect)
Expand Down Expand Up @@ -511,7 +544,7 @@ class AnimatePresence {
// This craziness is the only way I could figure out how to get the opacity
// accumulation to work in all browsers. Accumulating -1 onto opacity directly
// failed in both FF and Safari, and setting a delta to -1 still failed in FF
if (this.flow.animated && animateIn) {
if (this.flow.computedAnimated && animateIn) {
this.el.animate(
{
[opacityDeltaVar]: [-0.9999, 0]
Expand Down Expand Up @@ -539,7 +572,7 @@ class AnimatePresence {
if (this.#present === val) return
this.#present = val

if (!this.flow.animated) {
if (!this.flow.computedAnimated) {
if (!val) this.#remove()
return
}
Expand Down
6 changes: 6 additions & 0 deletions packages/number-flow/src/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { PartitionedParts } from './formatter'
import { charHeight, maskHeight, SlottedTag } from './styles'
import { BROWSER } from './util/env'

export const ServerSafeHTMLElement = BROWSER
? HTMLElement
: (class {} as unknown as typeof HTMLElement) // for types

export type RenderProps = { formatted: PartitionedParts['formatted']; willChange?: boolean }

// Could eventually use DSD e.g.
// `<template shadowroot="open" shadowrootmode="open">
export const render = ({ formatted, willChange }: RenderProps) =>
`<${SlottedTag} style="font-kerning: none; display: inline-block; line-height: ${charHeight}; padding: ${maskHeight} 0;${willChange ? 'will-change: transform' : ''}">${formatted}</${SlottedTag}>`
8 changes: 0 additions & 8 deletions packages/number-flow/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,6 @@ const scaledMaskWidth = `calc(${maskWidth} / var(--scale-x))`
const cornerGradient = `#000 0, transparent 71%` // or transparent ${maskWidth}

export const SlottedTag = 'span'
export const slottedStyles = ({ willChange }: { willChange?: boolean }) =>
({
fontKerning: 'none',
display: 'inline-block',
lineHeight: charHeight,
padding: `${maskHeight} 0`,
willChange: willChange ? 'transform' : undefined
}) as const

const styles = css`
:host {
Expand Down
2 changes: 2 additions & 0 deletions packages/number-flow/src/util/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ export const offset = (el: HTMLElement, justify: Justify) => {
el.offsetWidth -
el.offsetLeft
}

export const visible = (el: HTMLElement) => el.offsetWidth > 0 && el.offsetHeight > 0
Loading

0 comments on commit bced8ee

Please sign in to comment.