with the correct opacity', () => {
+ const waveElement = createWaveElement({ x: 0, y: 0 }, 0, {
+ initialOpacity: 0.5,
+ finalOpacity: 0.5,
+ } as IVWaveDirectiveOptions)
+
+ expect(waveElement.style.opacity).toBe('0.5')
+ })
+
+ test('returns a
with the correct transform', () => {
+ const waveElement = createWaveElement({ x: 0, y: 0 }, 0, {
+ initialOpacity: 0.5,
+ finalOpacity: 0.5,
+ } as IVWaveDirectiveOptions)
+
+ expect(waveElement.style.transform).toBe('translate(-50%,-50%) scale(0)')
+ })
+
+ test('returns a
with the correct transition', () => {
+ const waveElement = createWaveElement({ x: 0, y: 0 }, 0, {
+ duration: 1,
+ easing: 'ease-in',
+ } as IVWaveDirectiveOptions)
+
+ expect(waveElement.style.transition).toBe('transform 1s ease-in, opacity 1s ease-in')
+ })
})
diff --git a/src/utils/createWaveElement.ts b/src/utils/createWaveElement.ts
index 41a6a74..74139c3 100644
--- a/src/utils/createWaveElement.ts
+++ b/src/utils/createWaveElement.ts
@@ -1,6 +1,7 @@
import type { IVWaveDirectiveOptions } from '../options'
+import type { Vector } from '../types'
-export const createWaveElement = (x: number, y: number, size: number, options: IVWaveDirectiveOptions) => {
+export const createWaveElement = ({ x, y }: Vector, size: number, options: IVWaveDirectiveOptions) => {
const waveElement = document.createElement('div')
waveElement.style.position = 'absolute'
diff --git a/src/utils/getDistanceToFurthestCorner.test.ts b/src/utils/getDistanceToFurthestCorner.test.ts
index 09eb720..4132eed 100644
--- a/src/utils/getDistanceToFurthestCorner.test.ts
+++ b/src/utils/getDistanceToFurthestCorner.test.ts
@@ -2,6 +2,6 @@ import { expect, test } from 'vitest'
import { getDistanceToFurthestCorner } from './getDistanceToFurthestCorner'
test('getDistanceToFurthestCorner', () => {
- expect(getDistanceToFurthestCorner(25, 25, { width: 100, height: 100 } as DOMRect)).toBe(106.06601717798213)
- expect(getDistanceToFurthestCorner(25, 25, { width: 30, height: 30 } as DOMRect)).toBe(35.35533905932738)
+ expect(getDistanceToFurthestCorner({ x: 25, y: 25 }, { width: 100, height: 100 } as DOMRect)).toBe(106.06601717798213)
+ expect(getDistanceToFurthestCorner({ x: 25, y: 25 }, { width: 30, height: 30 } as DOMRect)).toBe(35.35533905932738)
})
diff --git a/src/utils/getDistanceToFurthestCorner.ts b/src/utils/getDistanceToFurthestCorner.ts
index e0cbc30..7129381 100644
--- a/src/utils/getDistanceToFurthestCorner.ts
+++ b/src/utils/getDistanceToFurthestCorner.ts
@@ -1,6 +1,7 @@
+import type { Vector } from '../types'
import { magnitude } from './magnitude'
-export function getDistanceToFurthestCorner(x: number, y: number, { width, height }: DOMRect) {
+export function getDistanceToFurthestCorner({ x, y }: Vector, { width, height }: DOMRect) {
const topLeft = magnitude(x, y, 0, 0)
const topRight = magnitude(x, y, width, 0)
const bottomLeft = magnitude(x, y, 0, height)
diff --git a/src/utils/getRelativePointer.ts b/src/utils/getRelativePointer.ts
index 60c51d4..bab39d2 100644
--- a/src/utils/getRelativePointer.ts
+++ b/src/utils/getRelativePointer.ts
@@ -1,4 +1,6 @@
-export const getRelativePointer = ({ x, y }: PointerEvent, { top, left }: DOMRect) => ({
+import type { Vector } from '../types'
+
+export const getRelativePointer = ({ x, y }: Vector, { top, left }: DOMRect): Vector => ({
x: x - left,
y: y - top,
})
diff --git a/src/utils/magnitude.test.ts b/src/utils/magnitude.test.ts
index 0e59db3..2e13857 100644
--- a/src/utils/magnitude.test.ts
+++ b/src/utils/magnitude.test.ts
@@ -3,4 +3,6 @@ import { magnitude } from './magnitude'
test('magnitude', () => {
expect(magnitude(5, 10, 10, 20)).toBe(11.180339887498949)
+
+ expect(magnitude(10, 20, 30, 40)).toBe(28.284271247461902)
})
diff --git a/src/utils/markWaveBoundary.test.ts b/src/utils/markWaveBoundary.test.ts
index dc5ce4f..16cfca8 100644
--- a/src/utils/markWaveBoundary.test.ts
+++ b/src/utils/markWaveBoundary.test.ts
@@ -5,11 +5,11 @@ describe('markWaveBoundary', () => {
test('sets dataset to true when trigger is not an id', () => {
const div = document.createElement('div')
markWaveBoundary(div, 'auto')
- expect(div).toMatchSnapshot()
+ expect(div.dataset.vWaveBoundary).toBe('true')
})
test('sets dataset to the id when the trigger is an id', () => {
const div = document.createElement('div')
markWaveBoundary(div, 'stringId')
- expect(div).toMatchSnapshot()
+ expect(div.dataset.vWaveBoundary).toBe('stringId')
})
})
diff --git a/src/utils/markWaveBoundary.ts b/src/utils/markWaveBoundary.ts
index 1799ca6..2b94580 100644
--- a/src/utils/markWaveBoundary.ts
+++ b/src/utils/markWaveBoundary.ts
@@ -1,5 +1,5 @@
import { triggerIsID } from './triggerIsID'
-export const markWaveBoundary = (el: HTMLElement, trigger: any) => {
- el.dataset.vWaveBoundary = triggerIsID(trigger) ? (trigger as string) : 'true'
+export const markWaveBoundary = (el: HTMLElement, trigger: string | boolean) => {
+ el.dataset.vWaveBoundary = triggerIsID(trigger) ? trigger : 'true'
}
diff --git a/src/utils/parentElementStyles.test.ts b/src/utils/parentElementStyles.test.ts
new file mode 100644
index 0000000..8e08a75
--- /dev/null
+++ b/src/utils/parentElementStyles.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, test, vi } from 'vitest'
+import { restoreParentElementStyles, saveParentElementStyles } from './parentElementStyles'
+
+describe('parentElementStyles', () => {
+ test('saveParentElementStyles', () => {
+ const el = document.createElement('div')
+ el.style.position = 'static'
+
+ saveParentElementStyles(el, window.getComputedStyle(el))
+
+ expect(el.style.position).toBe('relative')
+ expect(el.dataset.originalPositionValue).toBe('static')
+ })
+
+ test('restoreParentElementStyles', () => {
+ const el = document.createElement('div')
+ el.style.position = 'relative'
+ el.dataset.originalPositionValue = 'static'
+
+ restoreParentElementStyles(el)
+
+ expect(el.style.position).toBe('static')
+ expect(el.dataset.originalPositionValue).toBe(undefined)
+ })
+
+ test.each`
+ direction
+ ${'top'}
+ ${'left'}
+ ${'right'}
+ ${'bottom'}
+ `('saveParentElementStyles warns when position is static and $direction is not auto', ({ direction }) => {
+ const el = document.createElement('div')
+ el.style.position = 'static'
+ el.style[direction] = '10px'
+
+ console.warn = vi.fn()
+
+ saveParentElementStyles(el, window.getComputedStyle(el))
+
+ expect(console.warn).toHaveBeenCalledWith(
+ '[v-wave]:',
+ el,
+ `You're using a \`static\` positioned element with a non-auto value (10px) for \`${direction}\`.`,
+ "It's position will be changed to relative while displaying the wave which might cause the element to visually jump."
+ )
+ })
+})
diff --git a/src/utils/parentElementStyles.ts b/src/utils/parentElementStyles.ts
new file mode 100644
index 0000000..b947bde
--- /dev/null
+++ b/src/utils/parentElementStyles.ts
@@ -0,0 +1,21 @@
+export const saveParentElementStyles = (el: HTMLElement, computedStyles: CSSStyleDeclaration) => {
+ if (computedStyles.position === 'static') {
+ ;(['top', 'left', 'right', 'bottom'] as const).forEach((dir) => {
+ if (computedStyles[dir] && computedStyles[dir] !== 'auto')
+ console.warn(
+ '[v-wave]:',
+ el,
+ `You're using a \`static\` positioned element with a non-auto value (${computedStyles[dir]}) for \`${dir}\`.`,
+ "It's position will be changed to relative while displaying the wave which might cause the element to visually jump."
+ )
+ })
+
+ el.dataset.originalPositionValue = el.style.position
+ el.style.position = 'relative'
+ }
+}
+
+export const restoreParentElementStyles = (el: HTMLElement) => {
+ el.style.position = el.dataset.originalPositionValue ?? ''
+ delete el.dataset.originalPositionValue
+}
diff --git a/src/utils/triggerIsID.test.ts b/src/utils/triggerIsID.test.ts
index 692aabc..3dd4c6d 100644
--- a/src/utils/triggerIsID.test.ts
+++ b/src/utils/triggerIsID.test.ts
@@ -1,7 +1,7 @@
import { expect, test } from 'vitest'
import { triggerIsID } from './triggerIsID'
-test('markWaveBoundary', () => {
+test('triggerIsID', () => {
expect(triggerIsID('auto')).toEqual(false)
expect(triggerIsID(true)).toEqual(false)
expect(triggerIsID(false)).toEqual(false)
diff --git a/src/utils/triggerIsID.ts b/src/utils/triggerIsID.ts
index b54b4ba..35c5189 100644
--- a/src/utils/triggerIsID.ts
+++ b/src/utils/triggerIsID.ts
@@ -1 +1,2 @@
-export const triggerIsID = (trigger: string | boolean) => typeof trigger === 'string' && trigger !== 'auto'
+export const triggerIsID = (trigger: string | boolean): trigger is string /* and not 'auto' */ =>
+ typeof trigger === 'string' && trigger !== 'auto'
diff --git a/src/wave.ts b/src/wave.ts
index 0ab0e02..b63d3ad 100644
--- a/src/wave.ts
+++ b/src/wave.ts
@@ -1,36 +1,39 @@
import type { IVWaveDirectiveOptions } from './options'
+import type { Vector } from './types'
import { createContainer } from './utils/createContainerElement'
import { createWaveElement } from './utils/createWaveElement'
import { getDistanceToFurthestCorner } from './utils/getDistanceToFurthestCorner'
import { getRelativePointer } from './utils/getRelativePointer'
+import { restoreParentElementStyles, saveParentElementStyles } from './utils/parentElementStyles'
import { decrementWaveCount, deleteWaveCount, getWaveCount, incrementWaveCount } from './utils/wave-count'
-const wave = (event: PointerEvent, el: HTMLElement, options: IVWaveDirectiveOptions) => {
- if (options.disabled) return
+// 2.05 is magic.
+// Values smaller than this seem to cause the wave to stop
+// just short of the edge of the element sometimes.
+// (probably to floating point precision)
+const SCALE_FACTOR = 2.05
+const wave = (screenPos: Vector, el: HTMLElement, options: IVWaveDirectiveOptions) => {
+ if (options.disabled) return
if (options.respectDisabledAttribute && el.hasAttribute('disabled')) return
const rect = el.getBoundingClientRect()
const computedStyles = window.getComputedStyle(el)
- const { x, y } = getRelativePointer(event, rect)
- const size = 2.05 * getDistanceToFurthestCorner(x, y, rect) // 2.05 is magic, deal with it.
+ const relativePos = getRelativePointer(screenPos, rect)
+ const size = SCALE_FACTOR * getDistanceToFurthestCorner(relativePos, rect)
// We're creating a container for the "wave" with `overflow: hidden`
// because if we were to set `overflow: hidden` on `el` we
// risk altering its appearance.
const waveContainer = createContainer(computedStyles, options.tagName)
- const waveEl = createWaveElement(x, y, size, options)
+ const waveEl = createWaveElement(relativePos, size, options)
// Keep track of how many waves are active on this element.
incrementWaveCount(el)
// We reply on absolute positioning, so we need to make sure `el`'s position is non-static
- let originalPositionValue = ''
- if (computedStyles.position === 'static') {
- if (el.style.position) originalPositionValue = el.style.position
- el.style.position = 'relative'
- }
+ saveParentElementStyles(el, computedStyles)
waveContainer.appendChild(waveEl)
el.appendChild(waveContainer)
@@ -58,7 +61,7 @@ const wave = (event: PointerEvent, el: HTMLElement, options: IVWaveDirectiveOpti
if (getWaveCount(el) === 0) {
deleteWaveCount(el)
// Only reset the style after all active waves have been removed
- el.style.position = originalPositionValue
+ restoreParentElementStyles(el)
}
}, options.dissolveDuration * 1000)
}