diff --git a/biome.json b/biome.json index 73d11af..56d8c3c 100644 --- a/biome.json +++ b/biome.json @@ -7,17 +7,22 @@ "lineEnding": "crlf", "lineWidth": 120 }, - "organizeImports": { "enabled": true }, + "organizeImports": { + "enabled": true + }, "linter": { "enabled": true, "rules": { "recommended": true, "complexity": { - "noForEach": { "level": "off" }, - "useOptionalChain": { "level": "off" } + "noForEach": { + "level": "off" + } }, "suspicious": { - "noExplicitAny": { "level": "off" } + "noExplicitAny": { + "level": "off" + } } } }, @@ -30,6 +35,6 @@ } }, "files": { - "ignore": ["dist/**", "node_modules/**", "nuxt/**", "coverage/**"] + "ignore": ["dist/**", "node_modules/**", "nuxt/**", "coverage/**", ".vscode/**"] } } diff --git a/src/index.ts b/src/index.ts index 931baaf..fac3dd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,7 @@ const createDirective = ( [hooks.mounted](el, { value = {} }) { optionMap.set(el, value) - markWaveBoundary(el, (value && value.trigger) ?? globalOptions.trigger) + markWaveBoundary(el, value?.trigger ?? globalOptions.trigger) el.addEventListener('pointerdown', (event) => { if (!optionMap.has(el)) return @@ -62,7 +62,7 @@ const createDirective = ( }, [hooks.updated](el, { value = {} }) { optionMap.set(el, value) - markWaveBoundary(el, (value && value.trigger) ?? globalOptions.trigger) + markWaveBoundary(el, value?.trigger ?? globalOptions.trigger) }, } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2ae7aa6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export type Vector = { + x: number + y: number +} diff --git a/src/utils/__snapshots__/createContainerElement.test.ts.snap b/src/utils/__snapshots__/createContainerElement.test.ts.snap deleted file mode 100644 index 2c49e3a..0000000 --- a/src/utils/__snapshots__/createContainerElement.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`createContainerElement returns a an element based on \`tagName\` 1`] = ` -
-`; - -exports[`createContainerElement returns a an element based on \`tagName\` 2`] = ` - -`; diff --git a/src/utils/__snapshots__/createWaveElement.test.ts.snap b/src/utils/__snapshots__/createWaveElement.test.ts.snap deleted file mode 100644 index 56c0664..0000000 --- a/src/utils/__snapshots__/createWaveElement.test.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`createWaveElement returns a
1`] = ` -
-`; diff --git a/src/utils/__snapshots__/markWaveBoundary.test.ts.snap b/src/utils/__snapshots__/markWaveBoundary.test.ts.snap deleted file mode 100644 index a02827b..0000000 --- a/src/utils/__snapshots__/markWaveBoundary.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`markWaveBoundary > sets dataset to the id when the trigger is an id 1`] = ` -
-`; - -exports[`markWaveBoundary > sets dataset to true when trigger is not an id 1`] = ` -
-`; diff --git a/src/utils/createContainerElement.test.ts b/src/utils/createContainerElement.test.ts index 8c52f9a..c0c429e 100644 --- a/src/utils/createContainerElement.test.ts +++ b/src/utils/createContainerElement.test.ts @@ -1,7 +1,23 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { createContainer } from './createContainerElement' -test('createContainerElement returns a an element based on `tagName`', () => { - expect(createContainer({} as CSSStyleDeclaration, 'div')).toMatchSnapshot() - expect(createContainer({} as CSSStyleDeclaration, 'span')).toMatchSnapshot() +describe('createContainerElement', () => { + test('returns an element based on `tagName`', () => { + expect(createContainer({} as CSSStyleDeclaration, 'div').tagName).toBe('DIV') + expect(createContainer({} as CSSStyleDeclaration, 'span').tagName).toBe('SPAN') + }) + + test('returns an element with the correct border radius', () => { + const container = createContainer( + { + borderTopLeftRadius: '10px', + borderTopRightRadius: '20px', + borderBottomLeftRadius: '30px', + borderBottomRightRadius: '40px', + } as CSSStyleDeclaration, + 'div' + ) + + expect(container.style.borderRadius).toBe('10px 20px 40px 30px') + }) }) diff --git a/src/utils/createWaveElement.test.ts b/src/utils/createWaveElement.test.ts index 5bc091f..f351cc1 100644 --- a/src/utils/createWaveElement.test.ts +++ b/src/utils/createWaveElement.test.ts @@ -1,7 +1,64 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import type { IVWaveDirectiveOptions } from '../options' import { createWaveElement } from './createWaveElement' -test('createWaveElement returns a
', () => { - expect(createWaveElement(0, 0, 0, {} as IVWaveDirectiveOptions)).toMatchSnapshot() +describe('createWaveElement', () => { + test('returns a
', () => { + const waveElement = createWaveElement({ x: 0, y: 0 }, 0, {} as IVWaveDirectiveOptions) + + expect(waveElement.tagName).toBe('DIV') + }) + + test('returns a
with the correct position', () => { + const waveElement = createWaveElement({ x: 10, y: 20 }, 0, {} as IVWaveDirectiveOptions) + + expect(waveElement.style.top).toBe('20px') + expect(waveElement.style.left).toBe('10px') + }) + + test('returns a
with the correct size', () => { + const waveElement = createWaveElement({ x: 0, y: 0 }, 10, {} as IVWaveDirectiveOptions) + + expect(waveElement.style.width).toBe('10px') + expect(waveElement.style.height).toBe('10px') + }) + + test('returns a
with the correct background color', () => { + const waveElement = createWaveElement({ x: 0, y: 0 }, 0, { color: 'red' } as IVWaveDirectiveOptions) + + expect(waveElement.style.background).toBe('red') + }) + + test('returns a
with the correct border radius', () => { + const waveElement = createWaveElement({ x: 0, y: 0 }, 0, { color: 'red' } as IVWaveDirectiveOptions) + + expect(waveElement.style.borderRadius).toBe('50%') + }) + + test('returns a
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) }