@kine-design/core 0.0.1-beta.5 → 0.0.1-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/base/affix/useAffix.ts +2 -1
- package/components/base/anchor/useAnchor.ts +2 -1
- package/components/base/autoComplete/useAutoComplete.ts +2 -1
- package/components/base/carousel/useCarousel.ts +2 -1
- package/components/base/cascader/useCascader.ts +2 -1
- package/components/base/checkbox/useCheckbox.ts +2 -1
- package/components/base/collapse/useCollapse.ts +2 -1
- package/components/base/datePicker/__tests__/useDatePicker.test.ts +239 -0
- package/components/base/dropdown/useDropdown.ts +2 -1
- package/components/base/image/__tests__/useImage.test.ts +174 -0
- package/components/base/input/useInput.ts +3 -1
- package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
- package/components/base/inputNumber/useInputNumber.ts +53 -11
- package/components/base/popover/usePopover.ts +4 -3
- package/components/base/rate/useRate.ts +2 -1
- package/components/base/select/useSelect.ts +2 -1
- package/components/base/slider/useSlider.ts +2 -1
- package/components/base/steps/__tests__/useSteps.test.ts +46 -0
- package/components/base/switch/useSwitch.tsx +2 -1
- package/components/base/tabs/useTabs.ts +2 -1
- package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
- package/components/base/transfer/useTransfer.ts +2 -1
- package/components/base/tree/__tests__/tree.test.ts +214 -0
- package/components/message/notification/__tests__/useNotification.test.ts +129 -0
- package/components/message/popover/usePopover.ts +4 -4
- package/components/other/darkMode/useDarkMode.ts +2 -2
- package/components/template/menu/__tests__/useMenu.test.ts +157 -0
- package/components/template/pagination/__tests__/usePagination.test.ts +138 -0
- package/components/template/table/__tests__/useTable.test.ts +139 -0
- package/components/template/table/useTable.ts +2 -4
- package/components/types/hook.d.ts +11 -0
- package/compositions/common/__tests__/useDebounceFn.test.ts +62 -0
- package/compositions/common/__tests__/useEventListener.test.ts +71 -0
- package/compositions/common/__tests__/usePopover.test.ts +38 -0
- package/compositions/common/__tests__/useTeleport.test.ts +25 -0
- package/compositions/common/testAnchor.ts +211 -0
- package/compositions/common/useEventListener.ts +3 -3
- package/compositions/common/useResizeObserver.ts +6 -2
- package/compositions/input/__tests__/useBooleanInput.test.ts +73 -0
- package/compositions/modal/__tests__/useModal.test.ts +92 -0
- package/compositions/modal/useModal.ts +2 -2
- package/compositions/popper/useClickAway.ts +4 -4
- package/compositions/utils/__tests__/filters.test.ts +136 -0
- package/compositions/virtualList/__tests__/useHeightCache.test.ts +97 -0
- package/dist/components/base/affix/useAffix.d.ts +2 -1
- package/dist/components/base/anchor/useAnchor.d.ts +2 -1
- package/dist/components/base/autoComplete/useAutoComplete.d.ts +2 -1
- package/dist/components/base/carousel/useCarousel.d.ts +2 -1
- package/dist/components/base/cascader/useCascader.d.ts +2 -1
- package/dist/components/base/checkbox/useCheckbox.d.ts +2 -1
- package/dist/components/base/collapse/useCollapse.d.ts +2 -1
- package/dist/components/base/datePicker/__tests__/useDatePicker.test.d.ts +1 -0
- package/dist/components/base/dropdown/useDropdown.d.ts +2 -1
- package/dist/components/base/image/__tests__/useImage.test.d.ts +1 -0
- package/dist/components/base/input/useInput.d.ts +2 -1
- package/dist/components/base/inputNumber/__tests__/useInputNumber.test.d.ts +1 -0
- package/dist/components/base/inputNumber/useInputNumber.d.ts +2 -1
- package/dist/components/base/popover/usePopover.d.ts +2 -1
- package/dist/components/base/rate/useRate.d.ts +2 -1
- package/dist/components/base/select/useSelect.d.ts +2 -1
- package/dist/components/base/slider/useSlider.d.ts +2 -1
- package/dist/components/base/steps/__tests__/useSteps.test.d.ts +1 -0
- package/dist/components/base/switch/useSwitch.d.ts +2 -1
- package/dist/components/base/tabs/useTabs.d.ts +2 -1
- package/dist/components/base/timePicker/__tests__/useTimePicker.test.d.ts +1 -0
- package/dist/components/base/transfer/useTransfer.d.ts +2 -1
- package/dist/components/base/tree/__tests__/tree.test.d.ts +1 -0
- package/dist/components/message/notification/__tests__/useNotification.test.d.ts +1 -0
- package/dist/components/message/popover/usePopover.d.ts +1 -1
- package/dist/components/other/darkMode/useDarkMode.d.ts +2 -3
- package/dist/components/template/menu/__tests__/useMenu.test.d.ts +1 -0
- package/dist/components/template/pagination/__tests__/usePagination.test.d.ts +1 -0
- package/dist/components/template/table/__tests__/useTable.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useDebounceFn.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useEventListener.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/usePopover.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useTeleport.test.d.ts +1 -0
- package/dist/compositions/common/testAnchor.d.ts +40 -0
- package/dist/compositions/common/useEventListener.d.ts +2 -2
- package/dist/compositions/input/__tests__/useBooleanInput.test.d.ts +1 -0
- package/dist/compositions/modal/__tests__/useModal.test.d.ts +1 -0
- package/dist/compositions/modal/useModal.d.ts +2 -1
- package/dist/compositions/popper/useClickAway.d.ts +3 -3
- package/dist/compositions/utils/__tests__/filters.test.d.ts +1 -0
- package/dist/compositions/virtualList/__tests__/useHeightCache.test.d.ts +1 -0
- package/dist/core.js +64 -18
- package/dist/tools/__tests__/empty.test.d.ts +1 -0
- package/dist/tools/empty.d.ts +2 -2
- package/dist/tools/types.d.ts +1 -1
- package/dist/vitest.config.d.ts +10 -0
- package/package.json +6 -2
- package/tools/__tests__/empty.test.ts +72 -0
- package/tools/empty.ts +2 -2
- package/tools/types.ts +1 -1
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useDebounceFn 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import useDebounceFn from '../useDebounceFn';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
13
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
14
|
+
|
|
15
|
+
describe('useDebounceFn', () => {
|
|
16
|
+
it('返回防抖后的函数', () => {
|
|
17
|
+
const fn = vi.fn();
|
|
18
|
+
const debounced = useDebounceFn(fn, 100);
|
|
19
|
+
|
|
20
|
+
debounced();
|
|
21
|
+
expect(fn).not.toHaveBeenCalled();
|
|
22
|
+
|
|
23
|
+
vi.advanceTimersByTime(100);
|
|
24
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('默认延迟 200ms', () => {
|
|
28
|
+
const fn = vi.fn();
|
|
29
|
+
const debounced = useDebounceFn(fn);
|
|
30
|
+
|
|
31
|
+
debounced();
|
|
32
|
+
vi.advanceTimersByTime(199);
|
|
33
|
+
expect(fn).not.toHaveBeenCalled();
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(1);
|
|
36
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('支持 maxWait 选项', () => {
|
|
40
|
+
const fn = vi.fn();
|
|
41
|
+
const debounced = useDebounceFn(fn, 100, { maxWait: 150 });
|
|
42
|
+
|
|
43
|
+
debounced();
|
|
44
|
+
vi.advanceTimersByTime(80);
|
|
45
|
+
debounced();
|
|
46
|
+
vi.advanceTimersByTime(80);
|
|
47
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('多次调用只执行最后一次', () => {
|
|
51
|
+
const fn = vi.fn();
|
|
52
|
+
const debounced = useDebounceFn(fn, 50);
|
|
53
|
+
|
|
54
|
+
debounced('a');
|
|
55
|
+
debounced('b');
|
|
56
|
+
debounced('c');
|
|
57
|
+
|
|
58
|
+
vi.advanceTimersByTime(50);
|
|
59
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
60
|
+
expect(fn).toHaveBeenCalledWith('c');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useEventListener 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import useEventListener from '../useEventListener';
|
|
11
|
+
|
|
12
|
+
describe('useEventListener', () => {
|
|
13
|
+
it('add 注册事件,remove 移除事件', () => {
|
|
14
|
+
const handler = vi.fn();
|
|
15
|
+
const target = {
|
|
16
|
+
addEventListener: vi.fn(),
|
|
17
|
+
removeEventListener: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { add, remove } = useEventListener({
|
|
21
|
+
target,
|
|
22
|
+
event: 'click',
|
|
23
|
+
handler,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
add();
|
|
27
|
+
expect(target.addEventListener).toHaveBeenCalledWith('click', handler);
|
|
28
|
+
|
|
29
|
+
remove();
|
|
30
|
+
expect(target.removeEventListener).toHaveBeenCalledWith('click', handler);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('onMounted 调用 add,onBeforeUnmount 调用 remove', () => {
|
|
34
|
+
const handler = vi.fn();
|
|
35
|
+
const target = {
|
|
36
|
+
addEventListener: vi.fn(),
|
|
37
|
+
removeEventListener: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const { onMounted, onBeforeUnmount } = useEventListener({
|
|
41
|
+
target,
|
|
42
|
+
event: 'scroll',
|
|
43
|
+
handler,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
onMounted();
|
|
47
|
+
expect(target.addEventListener).toHaveBeenCalledOnce();
|
|
48
|
+
|
|
49
|
+
onBeforeUnmount();
|
|
50
|
+
expect(target.removeEventListener).toHaveBeenCalledOnce();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('target 为函数时延迟求值', () => {
|
|
54
|
+
const handler = vi.fn();
|
|
55
|
+
const realTarget = {
|
|
56
|
+
addEventListener: vi.fn(),
|
|
57
|
+
removeEventListener: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
const targetFn = vi.fn(() => realTarget);
|
|
60
|
+
|
|
61
|
+
const { add } = useEventListener({
|
|
62
|
+
target: targetFn,
|
|
63
|
+
event: 'click',
|
|
64
|
+
handler,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
add();
|
|
68
|
+
expect(targetFn).toHaveBeenCalled();
|
|
69
|
+
expect(realTarget.addEventListener).toHaveBeenCalledWith('click', handler);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description usePopover(common)测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import usePopover from '../usePopover';
|
|
11
|
+
|
|
12
|
+
describe('usePopover', () => {
|
|
13
|
+
it('默认 placement 为 bottom-start', () => {
|
|
14
|
+
const { popoverOptions } = usePopover();
|
|
15
|
+
expect(popoverOptions.placement).toBe('bottom-start');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('自定义 placement', () => {
|
|
19
|
+
const { popoverOptions } = usePopover({ placement: 'top-end' });
|
|
20
|
+
expect(popoverOptions.placement).toBe('top-end');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('默认 middleware 包含 offset、flip、shift', () => {
|
|
24
|
+
const { popoverOptions } = usePopover();
|
|
25
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(3);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('自定义 offset', () => {
|
|
29
|
+
const { popoverOptions } = usePopover(undefined, { offset: 10 });
|
|
30
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('扩展 middleware', () => {
|
|
34
|
+
const customMiddleware = { name: 'custom', fn: () => ({}) };
|
|
35
|
+
const { popoverOptions } = usePopover(undefined, { extends: [customMiddleware as any] });
|
|
36
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(4);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useTeleport / initTeleportOptions 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { initTeleportOptions } from '../useTeleport';
|
|
11
|
+
|
|
12
|
+
describe('initTeleportOptions', () => {
|
|
13
|
+
it('true 默认 teleport 到 body', () => {
|
|
14
|
+
expect(initTeleportOptions(true)).toEqual({ to: 'body' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('undefined 默认 teleport 到 body', () => {
|
|
18
|
+
expect(initTeleportOptions(undefined)).toEqual({ to: 'body' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('自定义配置直接返回', () => {
|
|
22
|
+
const custom = { to: '#app', disabled: false };
|
|
23
|
+
expect(initTeleportOptions(custom as any)).toBe(custom);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Test anchor system: provide/inject keys, types, and kDefineComponent wrapper
|
|
3
|
+
* @author kine-design
|
|
4
|
+
* @date 2026/4/28
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Renders business-semantic `data-k` attributes on DOM elements when testAnchor is enabled,
|
|
8
|
+
* allowing E2E tests to target components by stable identifiers.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
cloneVNode,
|
|
12
|
+
defineComponent,
|
|
13
|
+
inject,
|
|
14
|
+
type ComponentPropsOptions,
|
|
15
|
+
type EmitsOptions,
|
|
16
|
+
type InjectionKey,
|
|
17
|
+
type Ref,
|
|
18
|
+
type SetupContext,
|
|
19
|
+
type VNode,
|
|
20
|
+
} from 'vue';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Injection keys
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Global switch provided by KConfigProvider to enable data-k rendering */
|
|
27
|
+
export const K_TEST_ANCHOR_KEY: InjectionKey<Ref<boolean>> = Symbol('k-test-anchor');
|
|
28
|
+
|
|
29
|
+
/** Field identity provided by KFormItem so child inputs inherit field name */
|
|
30
|
+
export const K_FIELD_KEY: InjectionKey<Ref<string | undefined>> = Symbol('k-field');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface KAnchorConfig {
|
|
37
|
+
/** Semantic type: 'field' | 'action' | 'table' | 'col' | 'dialog' | 'drawer' | 'tab' */
|
|
38
|
+
type: string;
|
|
39
|
+
/** Which prop to derive the identity from (e.g., 'prop' for KFormItem, 'title' for KDialog) */
|
|
40
|
+
prop?: string;
|
|
41
|
+
/** Derive identity from default slot text content (for KButton) */
|
|
42
|
+
slotText?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface KDefineComponentOptions {
|
|
46
|
+
name: string;
|
|
47
|
+
props?: ComponentPropsOptions;
|
|
48
|
+
emits?: EmitsOptions;
|
|
49
|
+
kAnchor?: KAnchorConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract text content from a VNode tree (for slotText mode).
|
|
58
|
+
* Walks children recursively, concatenating string segments.
|
|
59
|
+
*/
|
|
60
|
+
function extractSlotText(nodes: VNode[] | undefined): string {
|
|
61
|
+
if (!nodes) return '';
|
|
62
|
+
let text = '';
|
|
63
|
+
for (const node of nodes) {
|
|
64
|
+
if (typeof node.children === 'string') {
|
|
65
|
+
text += node.children;
|
|
66
|
+
} else if (Array.isArray(node.children)) {
|
|
67
|
+
text += extractSlotText(node.children as VNode[]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return text.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the data-k value for a component instance.
|
|
75
|
+
* Returns undefined when testAnchor is off or no identity can be derived.
|
|
76
|
+
*/
|
|
77
|
+
function resolveDataK(
|
|
78
|
+
config: KAnchorConfig,
|
|
79
|
+
props: Record<string, unknown>,
|
|
80
|
+
attrs: Record<string, unknown>,
|
|
81
|
+
fieldFromParent: string | undefined,
|
|
82
|
+
slotNodes?: VNode[],
|
|
83
|
+
): string | undefined {
|
|
84
|
+
// Developer override via k-{type} attr (e.g., k-action="approve")
|
|
85
|
+
const overrideKey = `k-${config.type}`;
|
|
86
|
+
const override = attrs[overrideKey];
|
|
87
|
+
if (typeof override === 'string' && override) {
|
|
88
|
+
return `${config.type}:${override}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Derive from prop
|
|
92
|
+
if (config.prop) {
|
|
93
|
+
const val = props[config.prop];
|
|
94
|
+
if (typeof val === 'string' && val) {
|
|
95
|
+
return `${config.type}:${val}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Derive from slot text (e.g., KButton default slot)
|
|
100
|
+
if (config.slotText && slotNodes) {
|
|
101
|
+
const text = extractSlotText(slotNodes);
|
|
102
|
+
if (text) {
|
|
103
|
+
return `${config.type}:${text}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fallback: check props.text (for KButton text prop)
|
|
108
|
+
if (config.slotText && typeof props.text === 'string' && props.text) {
|
|
109
|
+
return `${config.type}:${props.text}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Inherit from parent KFormItem for input-type components
|
|
113
|
+
if (config.type === 'field' && !config.prop && fieldFromParent) {
|
|
114
|
+
return `${config.type}:${fieldFromParent}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Inject data-k attribute into a VNode.
|
|
122
|
+
* Uses cloneVNode to avoid mutating the original.
|
|
123
|
+
*/
|
|
124
|
+
function injectDataK(vnode: VNode, dataK: string): VNode {
|
|
125
|
+
return cloneVNode(vnode, { 'data-k': dataK });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// kDefineComponent
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Wrapper around Vue's defineComponent that automatically injects `data-k`
|
|
134
|
+
* test anchor attributes when testAnchor is enabled via KConfigProvider.
|
|
135
|
+
*
|
|
136
|
+
* Usage:
|
|
137
|
+
* ```tsx
|
|
138
|
+
* export default kDefineComponent((_props, ctx) => {
|
|
139
|
+
* return () => <div class="k-form-item">...</div>;
|
|
140
|
+
* }, {
|
|
141
|
+
* name: 'KFormItem',
|
|
142
|
+
* props: FormCore.formItemProps,
|
|
143
|
+
* kAnchor: { type: 'field', prop: 'prop' },
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function kDefineComponent<P extends Record<string, unknown>>(
|
|
148
|
+
setup: (props: P, ctx: SetupContext) => () => VNode | VNode[] | null | undefined,
|
|
149
|
+
options: KDefineComponentOptions,
|
|
150
|
+
) {
|
|
151
|
+
const { kAnchor, ...defineOptions } = options;
|
|
152
|
+
|
|
153
|
+
return defineComponent((rawProps: P, ctx: SetupContext) => {
|
|
154
|
+
// Call the original setup to get the render function
|
|
155
|
+
const render = setup(rawProps, ctx);
|
|
156
|
+
|
|
157
|
+
// If no kAnchor config, skip entirely
|
|
158
|
+
if (!kAnchor) {
|
|
159
|
+
return render;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read the global testAnchor switch
|
|
163
|
+
const testAnchor = inject(K_TEST_ANCHOR_KEY, undefined);
|
|
164
|
+
|
|
165
|
+
// For field-type components without own prop, inject parent field identity
|
|
166
|
+
const parentField = (kAnchor.type === 'field' && !kAnchor.prop)
|
|
167
|
+
? inject(K_FIELD_KEY, undefined)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
const vnode = render();
|
|
172
|
+
|
|
173
|
+
// When testAnchor is off (default), render as-is
|
|
174
|
+
if (!testAnchor?.value) {
|
|
175
|
+
return vnode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Resolve the data-k value
|
|
179
|
+
const props = rawProps as Record<string, unknown>;
|
|
180
|
+
const slotNodes = kAnchor.slotText ? (ctx.slots.default?.() ?? undefined) : undefined;
|
|
181
|
+
const dataK = resolveDataK(
|
|
182
|
+
kAnchor,
|
|
183
|
+
props,
|
|
184
|
+
ctx.attrs,
|
|
185
|
+
parentField?.value,
|
|
186
|
+
slotNodes,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!dataK) {
|
|
190
|
+
return vnode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Inject into root vnode
|
|
194
|
+
if (vnode === null || vnode === undefined) {
|
|
195
|
+
return vnode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle array of vnodes (Fragment) — inject into first element
|
|
199
|
+
if (Array.isArray(vnode)) {
|
|
200
|
+
if (vnode.length === 0) return vnode;
|
|
201
|
+
const first = vnode[0];
|
|
202
|
+
if (first && typeof first === 'object') {
|
|
203
|
+
return [injectDataK(first, dataK), ...vnode.slice(1)];
|
|
204
|
+
}
|
|
205
|
+
return vnode;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return injectDataK(vnode, dataK);
|
|
209
|
+
};
|
|
210
|
+
}, defineOptions as Parameters<typeof defineComponent>[1]);
|
|
211
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
export type EventListenerOptions = {
|
|
12
|
-
target:
|
|
12
|
+
target: EventTarget | (() => EventTarget),
|
|
13
13
|
event: string,
|
|
14
14
|
handler: EventListenerOrEventListenerObject,
|
|
15
15
|
};
|
|
@@ -35,14 +35,14 @@ export default function useEventListener(options: EventListenerOptions) {
|
|
|
35
35
|
add();
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const onBeforeUnmount = () => {
|
|
39
39
|
remove();
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
add, remove,
|
|
44
44
|
onMounted,
|
|
45
|
-
|
|
45
|
+
onBeforeUnmount,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Learn from vueuse useResizeObserver
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { Ref, watch } from 'vue';
|
|
12
|
+
import { onBeforeUnmount, Ref, watch } from 'vue';
|
|
13
13
|
|
|
14
14
|
export function useResizeObserver(
|
|
15
15
|
target: Ref<HTMLElement | null>,
|
|
@@ -27,7 +27,7 @@ export function useResizeObserver(
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// vueuse used watch
|
|
30
|
-
watch(target, () => {
|
|
30
|
+
const stopWatch = watch(target, () => {
|
|
31
31
|
if (target.value) {
|
|
32
32
|
cleanup();
|
|
33
33
|
observer = new ResizeObserver(callback);
|
|
@@ -35,6 +35,10 @@ export function useResizeObserver(
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
onBeforeUnmount(() => {
|
|
39
|
+
cleanup();
|
|
40
|
+
stopWatch();
|
|
41
|
+
});
|
|
38
42
|
|
|
39
43
|
return {
|
|
40
44
|
cleanup,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description initChecked / getNewModelValue 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { initChecked, getNewModelValue } from '../useBooleanInput';
|
|
11
|
+
|
|
12
|
+
describe('initChecked', () => {
|
|
13
|
+
it('checked 为 true 时直接返回 true', () => {
|
|
14
|
+
expect(initChecked({ checked: true, modelValue: undefined, value: undefined })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('modelValue 为 boolean 时返回该值', () => {
|
|
18
|
+
expect(initChecked({ checked: undefined, modelValue: true, value: undefined })).toBe(true);
|
|
19
|
+
expect(initChecked({ checked: undefined, modelValue: false, value: undefined })).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('modelValue 和 value 相等时返回 true', () => {
|
|
23
|
+
expect(initChecked({ checked: undefined, modelValue: 'a', value: 'a' })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('modelValue 和 value 不等时返回 false', () => {
|
|
27
|
+
expect(initChecked({ checked: undefined, modelValue: 'a', value: 'b' })).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('modelValue 为数组时,检查 value 是否在数组中', () => {
|
|
31
|
+
expect(initChecked({ checked: undefined, modelValue: ['a', 'b'], value: 'a' })).toBe(true);
|
|
32
|
+
expect(initChecked({ checked: undefined, modelValue: ['a', 'b'], value: 'c' })).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('所有值为空时返回 false', () => {
|
|
36
|
+
expect(initChecked({ checked: undefined, modelValue: undefined, value: undefined })).toBe(false);
|
|
37
|
+
expect(initChecked({ checked: null, modelValue: null, value: null })).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getNewModelValue', () => {
|
|
42
|
+
// 有 value 且 modelValue 为数组
|
|
43
|
+
it('checked=true 且 modelValue 为数组时添加 value', () => {
|
|
44
|
+
expect(getNewModelValue({ value: 'c', modelValue: ['a', 'b'] }, true)).toEqual(['a', 'b', 'c']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('checked=false 且 modelValue 为数组时移除 value', () => {
|
|
48
|
+
expect(getNewModelValue({ value: 'b', modelValue: ['a', 'b'] }, false)).toEqual(['a']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('不重复添加已存在的 value', () => {
|
|
52
|
+
expect(getNewModelValue({ value: 'a', modelValue: ['a', 'b'] }, true)).toEqual(['a', 'b']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('移除不存在的 value 不报错', () => {
|
|
56
|
+
expect(getNewModelValue({ value: 'c', modelValue: ['a', 'b'] }, false)).toEqual(['a', 'b']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 有 value 且 modelValue 非数组
|
|
60
|
+
it('checked=true 返回 value', () => {
|
|
61
|
+
expect(getNewModelValue({ value: 'a', modelValue: undefined }, true)).toBe('a');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('checked=false 返回 undefined', () => {
|
|
65
|
+
expect(getNewModelValue({ value: 'a', modelValue: undefined }, false)).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 无 value
|
|
69
|
+
it('无 value 时直接返回 checked 布尔值', () => {
|
|
70
|
+
expect(getNewModelValue({ value: undefined, modelValue: undefined }, true)).toBe(true);
|
|
71
|
+
expect(getNewModelValue({ value: undefined, modelValue: undefined }, false)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useModal 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { useModal } from '../useModal';
|
|
11
|
+
|
|
12
|
+
describe('useModal', () => {
|
|
13
|
+
it('初始 visible 默认为 false', () => {
|
|
14
|
+
const emit = vi.fn();
|
|
15
|
+
const { visible } = useModal({}, emit);
|
|
16
|
+
expect(visible.value).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('初始 visible 跟随 props', () => {
|
|
20
|
+
const emit = vi.fn();
|
|
21
|
+
const { visible } = useModal({ visible: true }, emit);
|
|
22
|
+
expect(visible.value).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('open 设置 visible 为 true 并触发 emit', () => {
|
|
26
|
+
const emit = vi.fn();
|
|
27
|
+
const { visible, open } = useModal({}, emit);
|
|
28
|
+
|
|
29
|
+
open();
|
|
30
|
+
expect(visible.value).toBe(true);
|
|
31
|
+
expect(emit).toHaveBeenCalledWith('update:visible', true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('close 设置 visible 为 false 并触发 emit', () => {
|
|
35
|
+
const emit = vi.fn();
|
|
36
|
+
const { visible, close } = useModal({ visible: true }, emit);
|
|
37
|
+
|
|
38
|
+
close();
|
|
39
|
+
expect(visible.value).toBe(false);
|
|
40
|
+
expect(emit).toHaveBeenCalledWith('update:visible', false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('toggle 切换 visible 状态', () => {
|
|
44
|
+
const emit = vi.fn();
|
|
45
|
+
const { visible, toggle } = useModal({}, emit);
|
|
46
|
+
|
|
47
|
+
toggle(); // false → true
|
|
48
|
+
expect(visible.value).toBe(true);
|
|
49
|
+
|
|
50
|
+
toggle(); // true → false
|
|
51
|
+
expect(visible.value).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('onMaskClick 默认关闭', () => {
|
|
55
|
+
const emit = vi.fn();
|
|
56
|
+
const { visible, open, onMaskClick } = useModal({}, emit);
|
|
57
|
+
|
|
58
|
+
open();
|
|
59
|
+
onMaskClick();
|
|
60
|
+
expect(visible.value).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('onMaskClick 当 mask.clickClose=false 时不关闭', () => {
|
|
64
|
+
const emit = vi.fn();
|
|
65
|
+
const { visible, open, onMaskClick } = useModal({ mask: { clickClose: false } }, emit);
|
|
66
|
+
|
|
67
|
+
open();
|
|
68
|
+
onMaskClick();
|
|
69
|
+
expect(visible.value).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('showMask 默认返回 true', () => {
|
|
73
|
+
const emit = vi.fn();
|
|
74
|
+
const { showMask } = useModal({}, emit);
|
|
75
|
+
expect(showMask()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('showMask 当 mask.show=false 时返回 false', () => {
|
|
79
|
+
const emit = vi.fn();
|
|
80
|
+
const { showMask } = useModal({ mask: { show: false } }, emit);
|
|
81
|
+
expect(showMask()).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('onContentClick 阻止冒泡', () => {
|
|
85
|
+
const emit = vi.fn();
|
|
86
|
+
const { onContentClick } = useModal({}, emit);
|
|
87
|
+
const event = { stopPropagation: vi.fn() } as unknown as MouseEvent;
|
|
88
|
+
|
|
89
|
+
onContentClick(event);
|
|
90
|
+
expect(event.stopPropagation).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 处理 visible 状态、遮罩层点击关闭、emit 同步
|
|
10
10
|
*/
|
|
11
11
|
import { ref, watch } from 'vue';
|
|
12
|
+
import { type HookContext } from '../../components/types/hook';
|
|
12
13
|
import { ModelMask } from '../../types/common/model';
|
|
13
14
|
|
|
14
15
|
export interface UseModalProps {
|
|
@@ -16,8 +17,7 @@ export interface UseModalProps {
|
|
|
16
17
|
mask?: ModelMask;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
export function useModal(props: UseModalProps, emit: (...args: any[]) => void) {
|
|
20
|
+
export function useModal(props: UseModalProps, emit: HookContext['emit']) {
|
|
21
21
|
const visible = ref(props.visible ?? false);
|
|
22
22
|
|
|
23
23
|
watch(() => props.visible, (val) => {
|
|
@@ -11,14 +11,14 @@ import useEventListener from '../common/useEventListener';
|
|
|
11
11
|
|
|
12
12
|
const event = 'pointerdown';
|
|
13
13
|
export default function useClickAway(options: {
|
|
14
|
-
target:
|
|
15
|
-
handler: (event:
|
|
14
|
+
target: EventTarget | (() => EventTarget | null | undefined),
|
|
15
|
+
handler: (event: PointerEvent) => void
|
|
16
16
|
}) {
|
|
17
17
|
if (typeof window === 'undefined' || !window) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const listener = (event:
|
|
21
|
+
const listener = (event: Event) => {
|
|
22
22
|
const el = typeof options.target === 'function' ? options.target() : options.target;
|
|
23
23
|
if (!el) {
|
|
24
24
|
return;
|
|
@@ -28,7 +28,7 @@ export default function useClickAway(options: {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
options.handler(event);
|
|
31
|
+
options.handler(event as PointerEvent);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
return useEventListener({
|