chore: 版本 0.1.0 更新补齐内置控件集 textarea/select/number/date/tags/between
Node.js Build / build (push) Successful in 6s
Node.js Build / build (push) Successful in 6s
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import js from '@eslint/js';
|
||||
|
||||
export default [
|
||||
{ ignores: ['**/node_modules/**', '**/dist/**', '**/storybook-static/**', '.storybook/**'] },
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{js,mjs}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
Generated
+22
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/vue3": "^8.6.14",
|
||||
"@storybook/vue3-vite": "^8.6.14",
|
||||
@@ -618,6 +619,27 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "http://verdaccio:4873/@eslint/js/-/js-10.0.1.tgz",
|
||||
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/vue3": "^8.6.14",
|
||||
"@storybook/vue3-vite": "^8.6.14",
|
||||
|
||||
+11
-6
@@ -7,9 +7,7 @@ import {
|
||||
createFieldState,
|
||||
groupRows,
|
||||
} from './composables/useFilters.js';
|
||||
import HeadlessInput from './components/HeadlessInput.vue';
|
||||
import UnknownPlaceholder from './components/UnknownPlaceholder.vue';
|
||||
|
||||
const props = defineProps({
|
||||
schema: { type: Array, default: () => [] },
|
||||
showReset: { type: Boolean, default: true },
|
||||
@@ -39,13 +37,15 @@ function resolveComponent(fieldDef, kind) {
|
||||
const name = typeof raw === 'string' ? raw : 'input';
|
||||
const mapped = config.value.componentMap[name];
|
||||
if (mapped) return mapped;
|
||||
if (name === 'input' || name === 'textarea' || name === 'number' || name === 'date') {
|
||||
return HeadlessInput;
|
||||
}
|
||||
console.warn(`[TransForm] unknown ${key}: "${name}"`);
|
||||
return UnknownPlaceholder;
|
||||
}
|
||||
|
||||
function isNullQuery(fieldDef) {
|
||||
const q = state[fieldDef.field]?.query;
|
||||
return q === 'IS_NULL' || q === 'IS_NOT_NULL';
|
||||
}
|
||||
|
||||
function emitChange() {
|
||||
emit('change', getFilters());
|
||||
}
|
||||
@@ -125,10 +125,15 @@ defineExpose({ getFilters, setFilters, loadFilters, reset });
|
||||
<component
|
||||
:is="resolveComponent(fieldDef, 'value')"
|
||||
v-model="state[fieldDef.field].value"
|
||||
:disabled="state[fieldDef.field]?.disabled"
|
||||
:disabled="state[fieldDef.field]?.disabled || isNullQuery(fieldDef)"
|
||||
:read-only="state[fieldDef.field]?.readOnly"
|
||||
:name="typeof fieldDef['value-component'] === 'string' ? fieldDef['value-component'] : 'custom'"
|
||||
:placeholder="fieldDef['field-name']"
|
||||
:field="fieldDef.field"
|
||||
:field-name="fieldDef['field-name']"
|
||||
:query-option="fieldDef['query-option'] || []"
|
||||
:schema="fieldDef"
|
||||
:options="fieldDef.options || []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const start = computed({
|
||||
get() {
|
||||
return String(props.modelValue ?? '').split(',')[0] ?? '';
|
||||
},
|
||||
set(value) {
|
||||
const endVal = String(props.modelValue ?? '').split(',')[1] ?? '';
|
||||
emit('update:modelValue', `${value ?? ''},${endVal}`);
|
||||
},
|
||||
});
|
||||
|
||||
const end = computed({
|
||||
get() {
|
||||
return String(props.modelValue ?? '').split(',')[1] ?? '';
|
||||
},
|
||||
set(value) {
|
||||
const startVal = String(props.modelValue ?? '').split(',')[0] ?? '';
|
||||
emit('update:modelValue', `${startVal},${value ?? ''}`);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-between-input"
|
||||
type="text"
|
||||
v-model="start"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<input
|
||||
class="tf-control tf-between-input"
|
||||
type="text"
|
||||
v-model="end"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-date"
|
||||
type="date"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
<template>
|
||||
<input
|
||||
class="tf-control tf-number"
|
||||
type="number"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
|
||||
const list = computed(() => (props.options?.length ? props.options : props.queryOption));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select
|
||||
class="tf-control tf-select"
|
||||
:value="modelValue"
|
||||
:disabled="disabled || readOnly"
|
||||
@change="$emit('update:modelValue', ($event.target).value)"
|
||||
>
|
||||
<option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
|
||||
<option v-for="opt in list" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const draft = ref('');
|
||||
|
||||
const tags = computed(() => {
|
||||
if (props.modelValue == null || props.modelValue === '') return [];
|
||||
return String(props.modelValue)
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0);
|
||||
});
|
||||
|
||||
function emitTags(next) {
|
||||
emit('update:modelValue', next.join(','));
|
||||
}
|
||||
|
||||
function commitDraft() {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const parts = draft.value
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length > 0);
|
||||
draft.value = '';
|
||||
if (!parts.length) return;
|
||||
emitTags([...tags.value, ...parts]);
|
||||
}
|
||||
|
||||
function removeTag(index) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const next = [...tags.value];
|
||||
next.splice(index, 1);
|
||||
emitTags(next);
|
||||
}
|
||||
|
||||
function onKeydown(event) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
if (event.key === 'Enter' || event.key === ',') {
|
||||
event.preventDefault();
|
||||
commitDraft();
|
||||
}
|
||||
}
|
||||
|
||||
function onInput(event) {
|
||||
if (props.disabled || props.readOnly) return;
|
||||
const value = event.target.value;
|
||||
draft.value = value;
|
||||
if (value.includes(',')) {
|
||||
commitDraft();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tf-control tf-tags" :class="{ 'is-disabled': disabled }">
|
||||
<span
|
||||
v-for="(tag, index) in tags"
|
||||
:key="`${tag}-${index}`"
|
||||
class="tf-tag"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
type="button"
|
||||
class="tf-tag-x"
|
||||
:disabled="disabled || readOnly"
|
||||
@click.stop="removeTag(index)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
class="tf-tag-input"
|
||||
type="text"
|
||||
:value="draft"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readOnly: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
name: { type: String, default: '' },
|
||||
field: { type: String, default: '' },
|
||||
fieldName: { type: String, default: '' },
|
||||
queryOption: { type: Array, default: () => [] },
|
||||
schema: { type: Object, default: () => ({}) },
|
||||
options: { type: Array, default: () => [] },
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
class="tf-control tf-textarea"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readOnly"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target).value)"
|
||||
></textarea>
|
||||
</template>
|
||||
+24
-2
@@ -1,8 +1,30 @@
|
||||
import HeadlessInput from './components/HeadlessInput.vue';
|
||||
import HeadlessTextarea from './components/HeadlessTextarea.vue';
|
||||
import HeadlessSelect from './components/HeadlessSelect.vue';
|
||||
import HeadlessNumber from './components/HeadlessNumber.vue';
|
||||
import HeadlessDate from './components/HeadlessDate.vue';
|
||||
import HeadlessTags from './components/HeadlessTags.vue';
|
||||
import HeadlessBetween from './components/HeadlessBetween.vue';
|
||||
|
||||
/**
|
||||
* 内置控件映射(Q5/Q58):字符串 value-component 先查这里,
|
||||
* 用户通过 setConfig({ componentMap }) 可覆盖或新增。
|
||||
*/
|
||||
export const BUILTIN_COMPONENT_MAP = {
|
||||
input: HeadlessInput,
|
||||
textarea: HeadlessTextarea,
|
||||
select: HeadlessSelect,
|
||||
number: HeadlessNumber,
|
||||
date: HeadlessDate,
|
||||
tags: HeadlessTags,
|
||||
between: HeadlessBetween,
|
||||
};
|
||||
|
||||
/** @typedef {{ componentMap?: Record<string, import('vue').Component>, colRatio?: { name?: number, query?: number, value?: number }, changeEmit?: 'immediate'|'debounce'|'manual' }} TransFormConfig */
|
||||
|
||||
/** @type {TransFormConfig & { componentMap: Record<string, import('vue').Component> }} */
|
||||
const globalConfig = {
|
||||
componentMap: {},
|
||||
componentMap: { ...BUILTIN_COMPONENT_MAP },
|
||||
colRatio: { name: 3, query: 2, value: 7 },
|
||||
changeEmit: 'immediate',
|
||||
};
|
||||
@@ -28,7 +50,7 @@ export function getConfig() {
|
||||
}
|
||||
|
||||
export function resetConfig() {
|
||||
globalConfig.componentMap = {};
|
||||
globalConfig.componentMap = { ...BUILTIN_COMPONENT_MAP };
|
||||
globalConfig.colRatio = { name: 3, query: 2, value: 7 };
|
||||
globalConfig.changeEmit = 'immediate';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import './styles/trans-form.css';
|
||||
|
||||
export { default as TransFormPlugin, TransForm, setConfig, getConfig } from './plugin.js';
|
||||
|
||||
export { default as HeadlessInput } from './components/HeadlessInput.vue';
|
||||
export { default as HeadlessTextarea } from './components/HeadlessTextarea.vue';
|
||||
export { default as HeadlessSelect } from './components/HeadlessSelect.vue';
|
||||
export { default as HeadlessNumber } from './components/HeadlessNumber.vue';
|
||||
export { default as HeadlessDate } from './components/HeadlessDate.vue';
|
||||
export { default as HeadlessTags } from './components/HeadlessTags.vue';
|
||||
export { default as HeadlessBetween } from './components/HeadlessBetween.vue';
|
||||
export { default as UnknownPlaceholder } from './components/UnknownPlaceholder.vue';
|
||||
|
||||
@@ -63,6 +63,67 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- tags(IN/NOT_IN 多值标签) ---- */
|
||||
.tf-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 32px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tf-tags.is-disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.tf-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #eef1f6;
|
||||
border: 1px solid var(--tf-border);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tf-tag-x {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--tf-label-color);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.tf-tag-x:hover {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
.tf-tag-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
font-size: 13px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* ---- between(区间双输入) ---- */
|
||||
.tf-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--tf-gap);
|
||||
}
|
||||
|
||||
.tf-between-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tf-actions {
|
||||
display: flex;
|
||||
gap: var(--tf-gap);
|
||||
|
||||
@@ -21,11 +21,63 @@ const schema = [
|
||||
{
|
||||
row: 6,
|
||||
logic: 'OR',
|
||||
'value-component': 'input',
|
||||
'value-component': 'select',
|
||||
field: 'status',
|
||||
'field-name': '状态',
|
||||
options: [
|
||||
{ value: 'active', label: '启用' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
],
|
||||
'query-option': [
|
||||
{ value: 'EQ', label: '=', 'desc-cn': '等于' },
|
||||
{ value: 'NE', label: '≠', 'desc-cn': '不等于' },
|
||||
],
|
||||
},
|
||||
{
|
||||
row: 4,
|
||||
logic: 'AND',
|
||||
'value-component': 'tags',
|
||||
field: 'tags',
|
||||
'field-name': '标签',
|
||||
'in-ui': 'tags',
|
||||
'query-option': [{ value: 'IN', label: '∈', 'desc-cn': '包含任一' }],
|
||||
},
|
||||
{
|
||||
row: 4,
|
||||
logic: 'AND',
|
||||
'value-component': 'between',
|
||||
field: 'created',
|
||||
'field-name': '创建时间',
|
||||
'between-ui': 'dual-input',
|
||||
'query-option': [{ value: 'BETWEEN', label: '↔', 'desc-cn': '区间' }],
|
||||
},
|
||||
{
|
||||
row: 4,
|
||||
logic: 'AND',
|
||||
'value-component': 'number',
|
||||
field: 'age',
|
||||
'field-name': '年龄',
|
||||
'query-option': [
|
||||
{ value: 'GT', label: '>', 'desc-cn': '大于' },
|
||||
{ value: 'LT', label: '<', 'desc-cn': '小于' },
|
||||
],
|
||||
},
|
||||
{
|
||||
row: 6,
|
||||
logic: 'AND',
|
||||
'value-component': 'date',
|
||||
field: 'birthday',
|
||||
'field-name': '生日',
|
||||
'query-option': [{ value: 'EQ', label: '=', 'desc-cn': '等于' }],
|
||||
},
|
||||
{
|
||||
row: 6,
|
||||
logic: 'AND',
|
||||
'value-component': 'textarea',
|
||||
field: 'remark',
|
||||
'field-name': '备注',
|
||||
'query-option': [{ value: 'LIKE', label: '~', 'desc-cn': '包含' }],
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import HeadlessTextarea from '../src/components/HeadlessTextarea.vue';
|
||||
import HeadlessSelect from '../src/components/HeadlessSelect.vue';
|
||||
import HeadlessNumber from '../src/components/HeadlessNumber.vue';
|
||||
import HeadlessDate from '../src/components/HeadlessDate.vue';
|
||||
import HeadlessTags from '../src/components/HeadlessTags.vue';
|
||||
import HeadlessBetween from '../src/components/HeadlessBetween.vue';
|
||||
|
||||
describe('HeadlessTextarea', () => {
|
||||
it('renders textarea and emits update:modelValue', async () => {
|
||||
const wrapper = mount(HeadlessTextarea, { props: { modelValue: 'hi' } });
|
||||
expect(wrapper.find('textarea').element.value).toBe('hi');
|
||||
await wrapper.find('textarea').setValue('hello');
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['hello']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeadlessSelect', () => {
|
||||
const options = [
|
||||
{ value: 'EQ', label: '=' },
|
||||
{ value: 'GT', label: '>' },
|
||||
];
|
||||
|
||||
it('renders options from props.options', () => {
|
||||
const wrapper = mount(HeadlessSelect, { props: { modelValue: 'EQ', options } });
|
||||
const opts = wrapper.findAll('option');
|
||||
expect(opts).toHaveLength(2);
|
||||
expect(opts[0].text()).toBe('=');
|
||||
});
|
||||
|
||||
it('falls back to queryOption when options empty', () => {
|
||||
const wrapper = mount(HeadlessSelect, {
|
||||
props: { modelValue: 'EQ', queryOption: options, options: [] },
|
||||
});
|
||||
expect(wrapper.findAll('option')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('emits on change', async () => {
|
||||
const wrapper = mount(HeadlessSelect, { props: { modelValue: 'EQ', options } });
|
||||
await wrapper.find('select').setValue('GT');
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['GT']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeadlessNumber', () => {
|
||||
it('emits number input', async () => {
|
||||
const wrapper = mount(HeadlessNumber, { props: { modelValue: 3 } });
|
||||
expect(wrapper.find('input').attributes('type')).toBe('number');
|
||||
await wrapper.find('input').setValue('7');
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['7']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeadlessDate', () => {
|
||||
it('renders date input', () => {
|
||||
const wrapper = mount(HeadlessDate, { props: { modelValue: '2026-08-07' } });
|
||||
expect(wrapper.find('input').attributes('type')).toBe('date');
|
||||
expect(wrapper.find('input').element.value).toBe('2026-08-07');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeadlessTags', () => {
|
||||
it('renders tags from comma string', () => {
|
||||
const wrapper = mount(HeadlessTags, { props: { modelValue: 'a,b,c' } });
|
||||
expect(wrapper.findAll('.tf-tag')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('adds tag on Enter', async () => {
|
||||
const wrapper = mount(HeadlessTags, { props: { modelValue: 'a' } });
|
||||
const input = wrapper.find('input');
|
||||
await input.setValue('b');
|
||||
await input.trigger('keydown', { key: 'Enter' });
|
||||
expect(wrapper.emitted('update:modelValue').at(-1)).toEqual(['a,b']);
|
||||
});
|
||||
|
||||
it('removes tag on x click', async () => {
|
||||
const wrapper = mount(HeadlessTags, { props: { modelValue: 'a,b' } });
|
||||
await wrapper.findAll('.tf-tag-x')[0].trigger('click');
|
||||
expect(wrapper.emitted('update:modelValue').at(-1)).toEqual(['b']);
|
||||
});
|
||||
|
||||
it('respects disabled', async () => {
|
||||
const wrapper = mount(HeadlessTags, { props: { modelValue: 'a', disabled: true } });
|
||||
await wrapper.find('input').trigger('keydown', { key: 'Enter' });
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeadlessBetween', () => {
|
||||
it('splits modelValue into two inputs', () => {
|
||||
const wrapper = mount(HeadlessBetween, { props: { modelValue: '1,10' } });
|
||||
const inputs = wrapper.findAll('input');
|
||||
expect(inputs).toHaveLength(2);
|
||||
expect(inputs[0].element.value).toBe('1');
|
||||
expect(inputs[1].element.value).toBe('10');
|
||||
});
|
||||
|
||||
it('emits combined string on start change', async () => {
|
||||
const wrapper = mount(HeadlessBetween, { props: { modelValue: '1,10' } });
|
||||
await wrapper.findAll('input')[0].setValue('5');
|
||||
expect(wrapper.emitted('update:modelValue').at(-1)).toEqual(['5,10']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user