mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 02:44:45 +00:00
feat: update package.json exports to support new module formats and types
This commit is contained in:
@@ -26,9 +26,14 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"import": {
|
||||||
"import": "./dist/index.mjs",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -23,9 +23,14 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"import": {
|
||||||
"import": "./dist/index.js",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -28,14 +28,24 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
"./browsers": {
|
"./browsers": {
|
||||||
"types": "./dist/browsers.d.ts",
|
"import": {
|
||||||
"import": "./dist/browsers.js",
|
"types": "./dist/browsers.d.mts",
|
||||||
"require": "./dist/browsers.cjs"
|
"default": "./dist/browsers.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/browsers.d.cts",
|
||||||
|
"default": "./dist/browsers.cjs"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"./multi": {
|
"./multi": {
|
||||||
"types": "./dist/multi.d.ts",
|
"import": {
|
||||||
"import": "./dist/multi.js",
|
"types": "./dist/multi.d.mts",
|
||||||
"require": "./dist/multi.cjs"
|
"default": "./dist/multi.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/multi.d.cts",
|
||||||
|
"default": "./dist/multi.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
139
core/platform/src/browsers/animationLifecycle/index.test.ts
Normal file
139
core/platform/src/browsers/animationLifecycle/index.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
getAnimationName,
|
||||||
|
isAnimatable,
|
||||||
|
shouldSuspendUnmount,
|
||||||
|
dispatchAnimationEvent,
|
||||||
|
onAnimationSettle,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
|
describe('getAnimationName', () => {
|
||||||
|
it('returns "none" for undefined element', () => {
|
||||||
|
expect(getAnimationName(undefined)).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the animation name from inline style', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.animationName = 'fadeIn';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
expect(getAnimationName(el)).toBe('fadeIn');
|
||||||
|
|
||||||
|
document.body.removeChild(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAnimatable', () => {
|
||||||
|
it('returns false for undefined element', () => {
|
||||||
|
expect(isAnimatable(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for element with no animation or transition', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
expect(isAnimatable(el)).toBe(false);
|
||||||
|
|
||||||
|
document.body.removeChild(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldSuspendUnmount', () => {
|
||||||
|
it('returns false for undefined element', () => {
|
||||||
|
expect(shouldSuspendUnmount(undefined, 'none')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for element with no animation/transition', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
expect(shouldSuspendUnmount(el, 'none')).toBe(false);
|
||||||
|
|
||||||
|
document.body.removeChild(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dispatchAnimationEvent', () => {
|
||||||
|
it('dispatches a custom event on the element', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
|
||||||
|
el.addEventListener('enter', handler);
|
||||||
|
dispatchAnimationEvent(el, 'enter');
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw for undefined element', () => {
|
||||||
|
expect(() => dispatchAnimationEvent(undefined, 'leave')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches non-bubbling event', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const parent = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
|
||||||
|
parent.appendChild(el);
|
||||||
|
parent.addEventListener('enter', handler);
|
||||||
|
dispatchAnimationEvent(el, 'enter');
|
||||||
|
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onAnimationSettle', () => {
|
||||||
|
it('returns a cleanup function', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const cleanup = onAnimationSettle(el, { onSettle: vi.fn() });
|
||||||
|
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSettle callback on transitionend', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
onAnimationSettle(el, { onSettle: callback });
|
||||||
|
el.dispatchEvent(new Event('transitionend'));
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSettle callback on transitioncancel', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
onAnimationSettle(el, { onSettle: callback });
|
||||||
|
el.dispatchEvent(new Event('transitioncancel'));
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onStart callback on animationstart', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const startCallback = vi.fn();
|
||||||
|
|
||||||
|
onAnimationSettle(el, {
|
||||||
|
onSettle: vi.fn(),
|
||||||
|
onStart: startCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('animationstart'));
|
||||||
|
|
||||||
|
expect(startCallback).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes all listeners on cleanup', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = onAnimationSettle(el, { onSettle: callback });
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('transitionend'));
|
||||||
|
el.dispatchEvent(new Event('transitioncancel'));
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
139
core/platform/src/browsers/animationLifecycle/index.ts
Normal file
139
core/platform/src/browsers/animationLifecycle/index.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
export type AnimationLifecycleEvent = 'enter' | 'after-enter' | 'leave' | 'after-leave';
|
||||||
|
|
||||||
|
export interface AnimationSettleCallbacks {
|
||||||
|
onSettle: () => void;
|
||||||
|
onStart?: (animationName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name getAnimationName
|
||||||
|
* @category Browsers
|
||||||
|
* @description Returns the current CSS animation name(s) of an element
|
||||||
|
*
|
||||||
|
* @since 0.0.5
|
||||||
|
*/
|
||||||
|
export function getAnimationName(el: HTMLElement | undefined): string {
|
||||||
|
return el ? getComputedStyle(el).animationName || 'none' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name isAnimatable
|
||||||
|
* @category Browsers
|
||||||
|
* @description Checks whether an element has a running CSS animation or transition
|
||||||
|
*
|
||||||
|
* @since 0.0.5
|
||||||
|
*/
|
||||||
|
export function isAnimatable(el: HTMLElement | undefined): boolean {
|
||||||
|
if (!el) return false;
|
||||||
|
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
const animationName = style.animationName || 'none';
|
||||||
|
const transitionProperty = style.transitionProperty || 'none';
|
||||||
|
|
||||||
|
const hasAnimation = animationName !== 'none' && animationName !== '';
|
||||||
|
const hasTransition = transitionProperty !== 'none' && transitionProperty !== '' && transitionProperty !== 'all';
|
||||||
|
|
||||||
|
return hasAnimation || hasTransition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name shouldSuspendUnmount
|
||||||
|
* @category Browsers
|
||||||
|
* @description Determines whether unmounting should be delayed due to a running animation/transition change
|
||||||
|
*
|
||||||
|
* @since 0.0.5
|
||||||
|
*/
|
||||||
|
export function shouldSuspendUnmount(el: HTMLElement | undefined, prevAnimationName: string): boolean {
|
||||||
|
if (!el) return false;
|
||||||
|
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
|
||||||
|
if (style.display === 'none') return false;
|
||||||
|
|
||||||
|
const animationName = style.animationName || 'none';
|
||||||
|
const transitionProperty = style.transitionProperty || 'none';
|
||||||
|
|
||||||
|
const hasAnimation = animationName !== 'none' && animationName !== '';
|
||||||
|
const hasTransition = transitionProperty !== 'none' && transitionProperty !== '' && transitionProperty !== 'all';
|
||||||
|
|
||||||
|
if (!hasAnimation && !hasTransition) return false;
|
||||||
|
|
||||||
|
return prevAnimationName !== animationName || hasTransition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name dispatchAnimationEvent
|
||||||
|
* @category Browsers
|
||||||
|
* @description Dispatches a non-bubbling custom event on an element for animation lifecycle tracking
|
||||||
|
*
|
||||||
|
* @since 0.0.5
|
||||||
|
*/
|
||||||
|
export function dispatchAnimationEvent(el: HTMLElement | undefined, name: AnimationLifecycleEvent): void {
|
||||||
|
el?.dispatchEvent(new CustomEvent(name, { bubbles: false, cancelable: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name onAnimationSettle
|
||||||
|
* @category Browsers
|
||||||
|
* @description Attaches animation/transition end listeners to an element with fill-mode flash prevention. Returns a cleanup function.
|
||||||
|
*
|
||||||
|
* @since 0.0.5
|
||||||
|
*/
|
||||||
|
export function onAnimationSettle(el: HTMLElement, callbacks: AnimationSettleCallbacks): () => void {
|
||||||
|
let fillModeTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const handleAnimationEnd = (event: AnimationEvent) => {
|
||||||
|
const currentAnimationName = getAnimationName(el);
|
||||||
|
const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName));
|
||||||
|
|
||||||
|
if (event.target === el && isCurrentAnimation) {
|
||||||
|
callbacks.onSettle();
|
||||||
|
|
||||||
|
if (fillModeTimeoutId !== undefined) {
|
||||||
|
clearTimeout(fillModeTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFillMode = el.style.animationFillMode;
|
||||||
|
el.style.animationFillMode = 'forwards';
|
||||||
|
|
||||||
|
fillModeTimeoutId = setTimeout(() => {
|
||||||
|
if (el.style.animationFillMode === 'forwards') {
|
||||||
|
el.style.animationFillMode = currentFillMode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (event.target === el && currentAnimationName === 'none') {
|
||||||
|
callbacks.onSettle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAnimationStart = (event: AnimationEvent) => {
|
||||||
|
if (event.target === el) {
|
||||||
|
callbacks.onStart?.(getAnimationName(el));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransitionEnd = (event: TransitionEvent) => {
|
||||||
|
if (event.target === el) {
|
||||||
|
callbacks.onSettle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('animationstart', handleAnimationStart, { passive: true });
|
||||||
|
el.addEventListener('animationcancel', handleAnimationEnd, { passive: true });
|
||||||
|
el.addEventListener('animationend', handleAnimationEnd, { passive: true });
|
||||||
|
el.addEventListener('transitioncancel', handleTransitionEnd, { passive: true });
|
||||||
|
el.addEventListener('transitionend', handleTransitionEnd, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('animationstart', handleAnimationStart);
|
||||||
|
el.removeEventListener('animationcancel', handleAnimationEnd);
|
||||||
|
el.removeEventListener('animationend', handleAnimationEnd);
|
||||||
|
el.removeEventListener('transitioncancel', handleTransitionEnd);
|
||||||
|
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||||
|
|
||||||
|
if (fillModeTimeoutId !== undefined) {
|
||||||
|
clearTimeout(fillModeTimeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from './animationLifecycle';
|
||||||
export * from './focusGuard';
|
export * from './focusGuard';
|
||||||
|
|||||||
@@ -28,9 +28,14 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"import": {
|
||||||
"import": "./dist/index.js",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -215,6 +215,9 @@ importers:
|
|||||||
|
|
||||||
vue/primitives:
|
vue/primitives:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@robonen/platform':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../core/platform
|
||||||
'@robonen/vue':
|
'@robonen/vue':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../toolkit
|
version: link:../toolkit
|
||||||
|
|||||||
@@ -25,9 +25,14 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.mts",
|
"import": {
|
||||||
"import": "./dist/index.mjs",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -51,6 +56,7 @@
|
|||||||
"vue-tsc": "^3.2.5"
|
"vue-tsc": "^3.2.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@robonen/platform": "workspace:*",
|
||||||
"@robonen/vue": "workspace:*",
|
"@robonen/vue": "workspace:*",
|
||||||
"@vue/shared": "catalog:",
|
"@vue/shared": "catalog:",
|
||||||
"vue": "catalog:"
|
"vue": "catalog:"
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import {
|
||||||
|
provideConfig,
|
||||||
|
provideAppConfig,
|
||||||
|
useConfig,
|
||||||
|
} from '..';
|
||||||
|
|
||||||
|
// --- useConfig ---
|
||||||
|
|
||||||
|
describe('useConfig', () => {
|
||||||
|
it('returns default config when no provider exists', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
const config = useConfig();
|
||||||
|
return { config };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div', {
|
||||||
|
'data-dir': this.config.dir.value,
|
||||||
|
'data-target': this.config.teleportTarget.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('div').attributes('data-dir')).toBe('ltr');
|
||||||
|
expect(wrapper.find('div').attributes('data-target')).toBe('body');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns custom config from provideConfig', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const config = useConfig();
|
||||||
|
return { config };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div', {
|
||||||
|
'data-dir': this.config.dir.value,
|
||||||
|
'data-target': this.config.teleportTarget.value,
|
||||||
|
'data-nonce': this.config.nonce.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Parent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
provideConfig({
|
||||||
|
dir: 'rtl',
|
||||||
|
teleportTarget: '#app',
|
||||||
|
nonce: 'abc123',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h(Child);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(Parent);
|
||||||
|
|
||||||
|
expect(wrapper.find('div').attributes('data-dir')).toBe('rtl');
|
||||||
|
expect(wrapper.find('div').attributes('data-target')).toBe('#app');
|
||||||
|
expect(wrapper.find('div').attributes('data-nonce')).toBe('abc123');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes mutable refs for runtime updates', async () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const config = useConfig();
|
||||||
|
return { config };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div', { 'data-dir': this.config.dir.value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Parent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const config = provideConfig({ dir: 'ltr' });
|
||||||
|
return { config };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h(Child);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(Parent);
|
||||||
|
expect(wrapper.find('div').attributes('data-dir')).toBe('ltr');
|
||||||
|
|
||||||
|
wrapper.vm.config.dir.value = 'rtl';
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.find('div').attributes('data-dir')).toBe('rtl');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- provideAppConfig ---
|
||||||
|
|
||||||
|
describe('provideAppConfig', () => {
|
||||||
|
it('provides config at app level', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const config = useConfig();
|
||||||
|
return { config };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div', {
|
||||||
|
'data-dir': this.config.dir.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(Child, {
|
||||||
|
global: {
|
||||||
|
plugins: [
|
||||||
|
app => provideAppConfig(app, { dir: 'rtl' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('div').attributes('data-dir')).toBe('rtl');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
45
vue/primitives/src/config-provider/context.ts
Normal file
45
vue/primitives/src/config-provider/context.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ref, shallowRef, toValue } from 'vue';
|
||||||
|
import type { App, MaybeRefOrGetter, Ref, ShallowRef, UnwrapRef } from 'vue';
|
||||||
|
import { useContextFactory } from '@robonen/vue';
|
||||||
|
|
||||||
|
export type Direction = 'ltr' | 'rtl';
|
||||||
|
|
||||||
|
export interface ConfigContext {
|
||||||
|
dir: Ref<Direction>;
|
||||||
|
nonce: Ref<string | undefined>;
|
||||||
|
teleportTarget: ShallowRef<string | HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigOptions {
|
||||||
|
dir?: MaybeRefOrGetter<Direction>;
|
||||||
|
nonce?: MaybeRefOrGetter<string | undefined>;
|
||||||
|
teleportTarget?: MaybeRefOrGetter<string | HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: UnwrapRef<ConfigContext> = {
|
||||||
|
dir: 'ltr',
|
||||||
|
nonce: undefined,
|
||||||
|
teleportTarget: 'body',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigCtx = useContextFactory<ConfigContext>('ConfigContext');
|
||||||
|
|
||||||
|
function resolveContext(options?: ConfigOptions): ConfigContext {
|
||||||
|
return {
|
||||||
|
dir: ref(toValue(options?.dir) ?? DEFAULT_CONFIG.dir),
|
||||||
|
nonce: ref(toValue(options?.nonce) ?? DEFAULT_CONFIG.nonce),
|
||||||
|
teleportTarget: shallowRef(toValue(options?.teleportTarget) ?? DEFAULT_CONFIG.teleportTarget),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provideConfig(options?: ConfigOptions): ConfigContext {
|
||||||
|
return ConfigCtx.provide(resolveContext(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provideAppConfig(app: App, options?: ConfigOptions): ConfigContext {
|
||||||
|
return ConfigCtx.appProvide(app)(resolveContext(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfig(): ConfigContext {
|
||||||
|
return ConfigCtx.inject(resolveContext());
|
||||||
|
}
|
||||||
8
vue/primitives/src/config-provider/index.ts
Normal file
8
vue/primitives/src/config-provider/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export {
|
||||||
|
provideConfig,
|
||||||
|
provideAppConfig,
|
||||||
|
useConfig,
|
||||||
|
type ConfigContext,
|
||||||
|
type ConfigOptions,
|
||||||
|
type Direction,
|
||||||
|
} from './context';
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './config-provider';
|
||||||
export * from './primitive';
|
export * from './primitive';
|
||||||
|
export * from './presence';
|
||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { useContextFactory } from '@robonen/vue';
|
import { useContextFactory } from '@robonen/vue';
|
||||||
|
|
||||||
export interface PaginationContext {
|
export interface PaginationContext {
|
||||||
currentPage: Ref<number>;
|
currentPage: Readonly<Ref<number>>;
|
||||||
totalPages: ComputedRef<number>;
|
totalPages: Readonly<Ref<number>>;
|
||||||
pageSize: Ref<number>;
|
pageSize: Readonly<Ref<number>>;
|
||||||
siblingCount: Ref<number>;
|
siblingCount: Readonly<Ref<number>>;
|
||||||
showEdges: Ref<boolean>;
|
showEdges: Readonly<Ref<boolean>>;
|
||||||
disabled: Ref<boolean>;
|
disabled: Readonly<Ref<boolean>>;
|
||||||
isFirstPage: ComputedRef<boolean>;
|
isFirstPage: Readonly<Ref<boolean>>;
|
||||||
isLastPage: ComputedRef<boolean>;
|
isLastPage: Readonly<Ref<boolean>>;
|
||||||
onPageChange: (value: number) => void;
|
onPageChange: (value: number) => void;
|
||||||
next: () => void;
|
next: () => void;
|
||||||
prev: () => void;
|
prev: () => void;
|
||||||
|
|||||||
28
vue/primitives/src/presence/Presence.vue
Normal file
28
vue/primitives/src/presence/Presence.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export interface PresenceProps {
|
||||||
|
present: boolean;
|
||||||
|
forceMount?: boolean;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePresence } from './usePresence';
|
||||||
|
|
||||||
|
const {
|
||||||
|
present,
|
||||||
|
forceMount = false,
|
||||||
|
} = defineProps<PresenceProps>();
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default?: (props: { present: boolean }) => any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { isPresent, setRef } = usePresence(() => present);
|
||||||
|
|
||||||
|
defineExpose({ present: isPresent });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- @vue-expect-error ref is forwarded to slot -->
|
||||||
|
<slot v-if="forceMount || present || isPresent" :ref="setRef" :present="isPresent" />
|
||||||
|
</template>
|
||||||
356
vue/primitives/src/presence/__test__/Presence.test.ts
Normal file
356
vue/primitives/src/presence/__test__/Presence.test.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { usePresence } from '../usePresence';
|
||||||
|
import Presence from '../Presence.vue';
|
||||||
|
import {
|
||||||
|
getAnimationName,
|
||||||
|
shouldSuspendUnmount,
|
||||||
|
dispatchAnimationEvent,
|
||||||
|
onAnimationSettle,
|
||||||
|
} from '@robonen/platform/browsers';
|
||||||
|
|
||||||
|
vi.mock('@robonen/platform/browsers', () => ({
|
||||||
|
getAnimationName: vi.fn(() => 'none'),
|
||||||
|
shouldSuspendUnmount: vi.fn(() => false),
|
||||||
|
dispatchAnimationEvent: vi.fn((el, name) => {
|
||||||
|
el?.dispatchEvent(new CustomEvent(name, { bubbles: false, cancelable: false }));
|
||||||
|
}),
|
||||||
|
onAnimationSettle: vi.fn(() => vi.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockGetAnimationName = vi.mocked(getAnimationName);
|
||||||
|
const mockShouldSuspend = vi.mocked(shouldSuspendUnmount);
|
||||||
|
const mockDispatchEvent = vi.mocked(dispatchAnimationEvent);
|
||||||
|
const mockOnSettle = vi.mocked(onAnimationSettle);
|
||||||
|
|
||||||
|
function mountUsePresence(initial: boolean) {
|
||||||
|
const present = ref(initial);
|
||||||
|
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { isPresent } = usePresence(present);
|
||||||
|
return { isPresent };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div', this.isPresent ? 'visible' : 'hidden');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { wrapper, present };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPresenceWithAnimation(present: Ref<boolean>) {
|
||||||
|
return mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { isPresent, setRef } = usePresence(present);
|
||||||
|
return { isPresent, setRef };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (!this.isPresent) return h('div', 'hidden');
|
||||||
|
|
||||||
|
return h('div', {
|
||||||
|
ref: (el: any) => this.setRef(el),
|
||||||
|
}, 'visible');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDispatchCall(name: string) {
|
||||||
|
return mockDispatchEvent.mock.calls.find(([, n]) => n === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('usePresence', () => {
|
||||||
|
it('returns isPresent=true when present is true', () => {
|
||||||
|
const { wrapper } = mountUsePresence(true);
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns isPresent=false when present is false', () => {
|
||||||
|
const { wrapper } = mountUsePresence(false);
|
||||||
|
expect(wrapper.text()).toBe('hidden');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transitions to unmounted immediately when no animation', async () => {
|
||||||
|
const { wrapper, present } = mountUsePresence(true);
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
|
||||||
|
present.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toBe('hidden');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transitions to mounted when present becomes true', async () => {
|
||||||
|
const { wrapper, present } = mountUsePresence(false);
|
||||||
|
expect(wrapper.text()).toBe('hidden');
|
||||||
|
|
||||||
|
present.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Presence', () => {
|
||||||
|
it('renders child when present is true', () => {
|
||||||
|
const wrapper = mount(Presence, {
|
||||||
|
props: { present: true },
|
||||||
|
slots: { default: () => h('div', 'content') },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.html()).toContain('content');
|
||||||
|
expect(wrapper.find('div').exists()).toBe(true);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render child when present is false', () => {
|
||||||
|
const wrapper = mount(Presence, {
|
||||||
|
props: { present: false },
|
||||||
|
slots: { default: () => h('div', 'content') },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.html()).not.toContain('content');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes child when present becomes false (no animation)', async () => {
|
||||||
|
const present = ref(true);
|
||||||
|
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => h(Presence, { present: present.value }, {
|
||||||
|
default: () => h('span', 'hello'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.find('span').exists()).toBe(true);
|
||||||
|
|
||||||
|
present.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.find('span').exists()).toBe(false);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds child when present becomes true', async () => {
|
||||||
|
const present = ref(false);
|
||||||
|
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => h(Presence, { present: present.value }, {
|
||||||
|
default: () => h('span', 'hello'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.find('span').exists()).toBe(false);
|
||||||
|
|
||||||
|
present.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.find('span').exists()).toBe(true);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always renders child when forceMount is true', () => {
|
||||||
|
const wrapper = mount(Presence, {
|
||||||
|
props: { present: false, forceMount: true },
|
||||||
|
slots: { default: () => h('span', 'always') },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('span').exists()).toBe(true);
|
||||||
|
expect(wrapper.find('span').text()).toBe('always');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes present state via scoped slot', () => {
|
||||||
|
let slotPresent: boolean | undefined;
|
||||||
|
|
||||||
|
const wrapper = mount(Presence, {
|
||||||
|
props: { present: true },
|
||||||
|
slots: {
|
||||||
|
default: (props: { present: boolean }) => {
|
||||||
|
slotPresent = props.present;
|
||||||
|
return h('div', 'content');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(slotPresent).toBe(true);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes present=false via scoped slot when forceMount and not present', () => {
|
||||||
|
let slotPresent: boolean | undefined;
|
||||||
|
|
||||||
|
const wrapper = mount(Presence, {
|
||||||
|
props: { present: false, forceMount: true },
|
||||||
|
slots: {
|
||||||
|
default: (props: { present: boolean }) => {
|
||||||
|
slotPresent = props.present;
|
||||||
|
return h('div', 'content');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(slotPresent).toBe(false);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('usePresence (animation)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetAnimationName.mockReturnValue('none');
|
||||||
|
mockShouldSuspend.mockReturnValue(false);
|
||||||
|
mockOnSettle.mockImplementation(() => vi.fn());
|
||||||
|
mockDispatchEvent.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches enter and after-enter when present becomes true (no animation)', async () => {
|
||||||
|
const present = ref(false);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
mockDispatchEvent.mockClear();
|
||||||
|
|
||||||
|
present.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findDispatchCall('enter')).toBeTruthy();
|
||||||
|
expect(findDispatchCall('after-enter')).toBeTruthy();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches leave and after-leave when no animation on leave', async () => {
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
mockDispatchEvent.mockClear();
|
||||||
|
|
||||||
|
present.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findDispatchCall('leave')).toBeTruthy();
|
||||||
|
expect(findDispatchCall('after-leave')).toBeTruthy();
|
||||||
|
expect(wrapper.text()).toBe('hidden');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('suspends unmount when shouldSuspendUnmount returns true', async () => {
|
||||||
|
mockShouldSuspend.mockReturnValue(true);
|
||||||
|
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
mockDispatchEvent.mockClear();
|
||||||
|
|
||||||
|
present.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findDispatchCall('leave')).toBeTruthy();
|
||||||
|
expect(findDispatchCall('after-leave')).toBeUndefined();
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches after-leave and unmounts when animation settles', async () => {
|
||||||
|
mockShouldSuspend.mockReturnValue(true);
|
||||||
|
|
||||||
|
let settleCallback: (() => void) | undefined;
|
||||||
|
mockOnSettle.mockImplementation((_el: any, callbacks: any) => {
|
||||||
|
settleCallback = callbacks.onSettle;
|
||||||
|
return vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
mockDispatchEvent.mockClear();
|
||||||
|
|
||||||
|
present.value = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
|
||||||
|
settleCallback!();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findDispatchCall('after-leave')).toBeTruthy();
|
||||||
|
expect(wrapper.text()).toBe('hidden');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks animation name on start via onAnimationSettle', async () => {
|
||||||
|
let startCallback: ((name: string) => void) | undefined;
|
||||||
|
mockOnSettle.mockImplementation((_el: any, callbacks: any) => {
|
||||||
|
startCallback = callbacks.onStart;
|
||||||
|
return vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(startCallback).toBeDefined();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls cleanup returned by onAnimationSettle on unmount', async () => {
|
||||||
|
const cleanupFn = vi.fn();
|
||||||
|
mockOnSettle.mockReturnValue(cleanupFn);
|
||||||
|
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setRef connects DOM element for animation tracking', async () => {
|
||||||
|
const present = ref(true);
|
||||||
|
const wrapper = mountPresenceWithAnimation(present);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
expect(mockOnSettle).toHaveBeenCalled();
|
||||||
|
expect(mockOnSettle.mock.calls[0]![0]).toBeInstanceOf(HTMLElement);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets isAnimating when node ref becomes undefined', async () => {
|
||||||
|
mockShouldSuspend.mockReturnValue(true);
|
||||||
|
|
||||||
|
mockOnSettle.mockImplementation(() => vi.fn());
|
||||||
|
|
||||||
|
const present = ref(true);
|
||||||
|
const showEl = ref(true);
|
||||||
|
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { isPresent, setRef } = usePresence(present);
|
||||||
|
return { isPresent, setRef, showEl };
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (!showEl.value) {
|
||||||
|
this.setRef(undefined);
|
||||||
|
return h('div', 'no-el');
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', {
|
||||||
|
ref: (el: any) => this.setRef(el),
|
||||||
|
}, this.isPresent ? 'visible' : 'hidden');
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
expect(wrapper.text()).toBe('visible');
|
||||||
|
|
||||||
|
showEl.value = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(wrapper.text()).toBe('no-el');
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
5
vue/primitives/src/presence/index.ts
Normal file
5
vue/primitives/src/presence/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as Presence } from './Presence.vue';
|
||||||
|
export { usePresence } from './usePresence';
|
||||||
|
|
||||||
|
export type { PresenceProps } from './Presence.vue';
|
||||||
|
export type { UsePresenceReturn } from './usePresence';
|
||||||
84
vue/primitives/src/presence/usePresence.ts
Normal file
84
vue/primitives/src/presence/usePresence.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { MaybeElement } from '@robonen/vue';
|
||||||
|
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||||
|
import { computed, readonly, shallowRef, toValue, watch } from 'vue';
|
||||||
|
import { tryOnScopeDispose, unrefElement } from '@robonen/vue';
|
||||||
|
import {
|
||||||
|
dispatchAnimationEvent,
|
||||||
|
getAnimationName,
|
||||||
|
onAnimationSettle,
|
||||||
|
shouldSuspendUnmount,
|
||||||
|
} from '@robonen/platform/browsers';
|
||||||
|
|
||||||
|
export interface UsePresenceReturn {
|
||||||
|
isPresent: Readonly<Ref<boolean>>;
|
||||||
|
setRef: (v: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePresence(
|
||||||
|
present: MaybeRefOrGetter<boolean>,
|
||||||
|
): UsePresenceReturn {
|
||||||
|
const node = shallowRef<HTMLElement>();
|
||||||
|
const isAnimating = shallowRef(false);
|
||||||
|
let prevAnimationName = 'none';
|
||||||
|
|
||||||
|
const isPresent = computed(() => toValue(present) || isAnimating.value);
|
||||||
|
|
||||||
|
watch(isPresent, (current) => {
|
||||||
|
prevAnimationName = current ? getAnimationName(node.value) : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => toValue(present), (value, oldValue) => {
|
||||||
|
if (value === oldValue) return;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
isAnimating.value = false;
|
||||||
|
dispatchAnimationEvent(node.value, 'enter');
|
||||||
|
|
||||||
|
if (getAnimationName(node.value) === 'none') {
|
||||||
|
dispatchAnimationEvent(node.value, 'after-enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isAnimating.value = shouldSuspendUnmount(node.value, prevAnimationName);
|
||||||
|
dispatchAnimationEvent(node.value, 'leave');
|
||||||
|
|
||||||
|
if (!isAnimating.value) {
|
||||||
|
dispatchAnimationEvent(node.value, 'after-leave');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { flush: 'sync' });
|
||||||
|
|
||||||
|
watch(node, (el, _oldEl, onCleanup) => {
|
||||||
|
if (el) {
|
||||||
|
const cleanup = onAnimationSettle(el, {
|
||||||
|
onSettle: () => {
|
||||||
|
const direction = toValue(present) ? 'enter' : 'leave';
|
||||||
|
dispatchAnimationEvent(el, `after-${direction}`);
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
onStart: (animationName) => {
|
||||||
|
prevAnimationName = animationName;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(cleanup);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isAnimating.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tryOnScopeDispose(() => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function setRef(v: unknown) {
|
||||||
|
const el = unrefElement(v as MaybeElement);
|
||||||
|
node.value = el instanceof HTMLElement ? el : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPresent: readonly(isPresent),
|
||||||
|
setRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Component, IntrinsicElementAttributes, SetupContext } from 'vue';
|
import type { AllowedComponentProps, Component, IntrinsicElementAttributes, SetupContext, VNodeProps } from 'vue';
|
||||||
import { h } from 'vue';
|
import { h, mergeProps } from 'vue';
|
||||||
import { Slot } from './Slot';
|
import { Slot } from './Slot';
|
||||||
|
|
||||||
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
|
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
|
||||||
@@ -8,10 +8,12 @@ export interface PrimitiveProps {
|
|||||||
as?: keyof IntrinsicElementAttributes | Component;
|
as?: keyof IntrinsicElementAttributes | Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Primitive(props: PrimitiveProps, ctx: FunctionalComponentContext) {
|
export function Primitive(props: PrimitiveProps & VNodeProps & AllowedComponentProps & Record<string, unknown>, ctx: FunctionalComponentContext) {
|
||||||
return props.as === 'template'
|
const { as, ...delegatedProps } = props;
|
||||||
? h(Slot, ctx.attrs, ctx.slots)
|
|
||||||
: h(props.as!, ctx.attrs, ctx.slots);
|
return as === 'template'
|
||||||
|
? h(Slot, mergeProps(ctx.attrs, delegatedProps), ctx.slots)
|
||||||
|
: h(as!, mergeProps(ctx.attrs, delegatedProps), ctx.slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
Primitive.props = {
|
Primitive.props = {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
},
|
},
|
||||||
"vueCompilerOptions": {
|
"vueCompilerOptions": {
|
||||||
"strictTemplates": true,
|
"strictTemplates": true,
|
||||||
"fallthroughAttributes": true
|
"fallthroughAttributes": true,
|
||||||
|
"inferTemplateDollarAttrs": true,
|
||||||
|
"inferTemplateDollarEl": true,
|
||||||
|
"inferTemplateDollarRefs": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,9 +26,14 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"import": {
|
||||||
"import": "./dist/index.js",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from 'vue';
|
|||||||
import { toValue } from 'vue';
|
import { toValue } from 'vue';
|
||||||
|
|
||||||
export type VueInstance = ComponentPublicInstance;
|
export type VueInstance = ComponentPublicInstance;
|
||||||
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
|
export type MaybeElement = Element | VueInstance | undefined | null;
|
||||||
|
|
||||||
export type MaybeElementRef<El extends MaybeElement = MaybeElement> = MaybeRef<El>;
|
export type MaybeElementRef<El extends MaybeElement = MaybeElement> = MaybeRef<El>;
|
||||||
export type MaybeComputedElementRef<El extends MaybeElement = MaybeElement> = MaybeRefOrGetter<El>;
|
export type MaybeComputedElementRef<El extends MaybeElement = MaybeElement> = MaybeRefOrGetter<El>;
|
||||||
|
|||||||
Reference in New Issue
Block a user