@zap-wunschlachen/wl-shared-components 1.0.87 → 1.0.89
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/.github/workflows/unit.yml +97 -0
- package/App.vue +1 -1
- package/package.json +5 -4
- package/src/components/Input/Input.vue +11 -11
- package/src/components/Input/presets.ts +60 -0
- package/src/components/Select/Select.vue +31 -0
- package/src/components/index.ts +1 -0
- package/src/i18n/locales/de.json +3 -0
- package/src/i18n/locales/en.json +3 -0
- package/src/pages/SelectPage.vue +166 -0
- package/src/pages/ValidatedInputPage.vue +584 -249
- package/tests/unit/accessibility/patient-app-a11y.spec.ts +5 -5
- package/tests/unit/components/Accordion/AccordionItem.spec.ts +15 -12
- package/tests/unit/components/Appointment/Card/Details.spec.ts +8 -3
- package/tests/unit/components/Core/Input.spec.ts +281 -5
- package/tests/unit/components/Icons/calendar.spec.ts +1 -1
- package/tests/unit/components/Icons/play.spec.ts +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [develop]
|
|
6
|
+
paths:
|
|
7
|
+
- "src/**"
|
|
8
|
+
- "tests/unit/**"
|
|
9
|
+
- "package.json"
|
|
10
|
+
- "package-lock.json"
|
|
11
|
+
- "tsconfig.json"
|
|
12
|
+
- "vitest.config.ts"
|
|
13
|
+
- "vite.config.ts"
|
|
14
|
+
- ".github/workflows/unit.yml"
|
|
15
|
+
pull_request:
|
|
16
|
+
branches: [develop]
|
|
17
|
+
paths:
|
|
18
|
+
- "src/**"
|
|
19
|
+
- "tests/unit/**"
|
|
20
|
+
- "package.json"
|
|
21
|
+
- "package-lock.json"
|
|
22
|
+
- "tsconfig.json"
|
|
23
|
+
- "vitest.config.ts"
|
|
24
|
+
- "vite.config.ts"
|
|
25
|
+
- ".github/workflows/unit.yml"
|
|
26
|
+
workflow_dispatch:
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
checks: write
|
|
31
|
+
|
|
32
|
+
concurrency:
|
|
33
|
+
group: unit-${{ github.head_ref || github.ref_name }}
|
|
34
|
+
cancel-in-progress: true
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
unit-tests:
|
|
38
|
+
name: Unit Tests
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
timeout-minutes: 15
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: '20'
|
|
48
|
+
cache: 'npm'
|
|
49
|
+
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
run: npm ci
|
|
52
|
+
|
|
53
|
+
- name: Run unit tests
|
|
54
|
+
run: npx vitest run --reporter=dot --reporter=github-actions --reporter=junit --reporter=json --outputFile.junit=test-results/vitest-junit.xml --outputFile.json=test-results/vitest-results.json
|
|
55
|
+
|
|
56
|
+
- name: Test Report
|
|
57
|
+
uses: dorny/test-reporter@v2
|
|
58
|
+
if: always()
|
|
59
|
+
with:
|
|
60
|
+
name: Vitest Results
|
|
61
|
+
path: test-results/vitest-junit.xml
|
|
62
|
+
reporter: java-junit
|
|
63
|
+
|
|
64
|
+
- name: Upload test results
|
|
65
|
+
uses: actions/upload-artifact@v4
|
|
66
|
+
if: always()
|
|
67
|
+
with:
|
|
68
|
+
name: test-results
|
|
69
|
+
path: test-results/
|
|
70
|
+
retention-days: 7
|
|
71
|
+
|
|
72
|
+
coverage:
|
|
73
|
+
name: Coverage
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
timeout-minutes: 15
|
|
76
|
+
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
|
|
80
|
+
- uses: actions/setup-node@v4
|
|
81
|
+
with:
|
|
82
|
+
node-version: '20'
|
|
83
|
+
cache: 'npm'
|
|
84
|
+
|
|
85
|
+
- name: Install dependencies
|
|
86
|
+
run: npm ci
|
|
87
|
+
|
|
88
|
+
- name: Run tests with coverage
|
|
89
|
+
run: npx vitest run --coverage
|
|
90
|
+
|
|
91
|
+
- name: Upload coverage report
|
|
92
|
+
uses: actions/upload-artifact@v4
|
|
93
|
+
if: always()
|
|
94
|
+
with:
|
|
95
|
+
name: coverage
|
|
96
|
+
path: coverage/
|
|
97
|
+
retention-days: 7
|
package/App.vue
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-wunschlachen/wl-shared-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.89",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@axe-core/playwright": "^4.10.2",
|
|
30
|
-
"@types/dompurify": "^3.0.5",
|
|
31
30
|
"@chromatic-com/storybook": "^1.9.0",
|
|
32
31
|
"@playwright/test": "^1.55.0",
|
|
33
32
|
"@storybook/addon-actions": "^8.3.5",
|
|
@@ -42,6 +41,7 @@
|
|
|
42
41
|
"@storybook/vue3-vite": "^8.3.5",
|
|
43
42
|
"@testing-library/user-event": "^14.6.1",
|
|
44
43
|
"@testing-library/vue": "^8.1.0",
|
|
44
|
+
"@types/dompurify": "^3.0.5",
|
|
45
45
|
"@vitest/coverage-v8": "^3.2.4",
|
|
46
46
|
"@vitest/ui": "^3.2.4",
|
|
47
47
|
"@vue/test-utils": "^2.4.6",
|
|
@@ -51,17 +51,18 @@
|
|
|
51
51
|
"playwright": "^1.55.0",
|
|
52
52
|
"prettier": "^3.3.1",
|
|
53
53
|
"storybook": "^8.3.5",
|
|
54
|
+
"tsx": "^4.21.0",
|
|
54
55
|
"typescript": "^5.2.2",
|
|
55
56
|
"vite": "^5.2.0",
|
|
56
57
|
"vitest": "^3.2.4",
|
|
57
58
|
"vue": "^3.5.11",
|
|
58
|
-
"wait-on": "^8.0.4"
|
|
59
|
-
"tsx": "^4.21.0"
|
|
59
|
+
"wait-on": "^8.0.4"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@heroicons/vue": "^2.1.5",
|
|
63
63
|
"@mdi/font": "^7.4.47",
|
|
64
64
|
"@vitejs/plugin-vue": "^5.1.4",
|
|
65
|
+
"awesome-phonenumber": "^7.8.0",
|
|
65
66
|
"date-fns": "^4.1.0",
|
|
66
67
|
"dompurify": "^3.2.4",
|
|
67
68
|
"flag-icons": "^7.5.0",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
import { ref, nextTick, computed, inject, watch } from 'vue';
|
|
67
67
|
import './Input.css';
|
|
68
68
|
import { useI18n } from 'vue-i18n';
|
|
69
|
+
import { resolvePreset } from './presets';
|
|
69
70
|
import { siteColors } from "../../utils/index";
|
|
70
71
|
|
|
71
72
|
// Inject theme colors from ThemeProvider, fallback to global siteColors
|
|
@@ -83,6 +84,7 @@ const { t } = useI18n();
|
|
|
83
84
|
const emit = defineEmits([
|
|
84
85
|
'update:modelValue', // Emits when the input value is updated
|
|
85
86
|
'update:valid', // Emits when the validation state changes
|
|
87
|
+
'update:formatted', // Emits formatted value when preset has a format function
|
|
86
88
|
'click:prepend', // Emits when the prepend icon is clicked
|
|
87
89
|
'click:append', // Emits when the append icon is clicked
|
|
88
90
|
'click:prepend-inner', // Emits when the prepend inner icon is clicked
|
|
@@ -96,8 +98,6 @@ const emit = defineEmits([
|
|
|
96
98
|
// Generate unique ID for accessibility - connects input to hint/error message
|
|
97
99
|
const inputHintId = `input-hint-${Math.random().toString(36).slice(2, 11)}`;
|
|
98
100
|
|
|
99
|
-
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
100
|
-
|
|
101
101
|
// Define the component props
|
|
102
102
|
const props = defineProps({
|
|
103
103
|
/**
|
|
@@ -217,12 +217,13 @@ const props = defineProps({
|
|
|
217
217
|
},
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
* Activates a built-in validation ruleset.
|
|
220
|
+
* Activates a built-in validation ruleset. Pass a string shorthand ('email', 'phone'),
|
|
221
|
+
* a preset factory result, or a custom { rules, format? } object.
|
|
221
222
|
*/
|
|
222
223
|
preset: {
|
|
223
|
-
type: String,
|
|
224
|
+
type: [String, Object],
|
|
224
225
|
default: undefined,
|
|
225
|
-
validator: (v) =>
|
|
226
|
+
validator: (v) => v === undefined || typeof v === 'string' || (typeof v === 'object' && Array.isArray(v.rules)),
|
|
226
227
|
},
|
|
227
228
|
|
|
228
229
|
/**
|
|
@@ -246,12 +247,8 @@ const props = defineProps({
|
|
|
246
247
|
// --- Validation logic ---
|
|
247
248
|
const validationError = ref('');
|
|
248
249
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
return [(value) => EMAIL_REGEX.test(value) || t('wl.input.email_error', 'Ungültige E-Mail-Adresse')];
|
|
252
|
-
}
|
|
253
|
-
return [];
|
|
254
|
-
});
|
|
250
|
+
const resolvedPreset = computed(() => props.preset ? resolvePreset(props.preset, t) : null);
|
|
251
|
+
const presetRules = computed(() => resolvedPreset.value?.rules ?? []);
|
|
255
252
|
|
|
256
253
|
const allRules = computed(() => [...presetRules.value, ...props.rules]);
|
|
257
254
|
|
|
@@ -329,6 +326,9 @@ const onAppendInnerClick = () => {
|
|
|
329
326
|
// Emit the updated value when the input changes, syncing with v-model
|
|
330
327
|
const onInput = (value) => {
|
|
331
328
|
emit('update:modelValue', value);
|
|
329
|
+
if (resolvedPreset.value?.format) {
|
|
330
|
+
emit('update:formatted', resolvedPreset.value.format(value));
|
|
331
|
+
}
|
|
332
332
|
if (allRules.value.length > 0 && props.validateOn === 'input') {
|
|
333
333
|
validate(value);
|
|
334
334
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parsePhoneNumber } from 'awesome-phonenumber';
|
|
2
|
+
|
|
3
|
+
export interface InputPreset {
|
|
4
|
+
rules: ((value: string) => true | string)[];
|
|
5
|
+
format?: (value: string) => string | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type TranslateFn = (key: string, fallback: string) => string;
|
|
9
|
+
|
|
10
|
+
const defaultT: TranslateFn = (_key, fallback) => fallback;
|
|
11
|
+
|
|
12
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
13
|
+
|
|
14
|
+
export function emailPreset(t: TranslateFn = defaultT): InputPreset {
|
|
15
|
+
return {
|
|
16
|
+
rules: [
|
|
17
|
+
(value) => EMAIL_REGEX.test(value) || t('wl.input.email_error', 'Ungültige E-Mail-Adresse'),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PhonePresetOptions {
|
|
23
|
+
region?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function phonePreset(options?: PhonePresetOptions, t: TranslateFn = defaultT): InputPreset {
|
|
27
|
+
const region = options?.region ?? 'DE';
|
|
28
|
+
return {
|
|
29
|
+
rules: [
|
|
30
|
+
(value) => {
|
|
31
|
+
if (!/^[+\d\s\-()/.]+$/.test(value)) return t('wl.input.phone_error', 'Ungültige Telefonnummer');
|
|
32
|
+
if (!/^[+0]/.test(value.trim())) return t('wl.input.phone_error', 'Ungültige Telefonnummer');
|
|
33
|
+
const pn = parsePhoneNumber(value, { regionCode: region });
|
|
34
|
+
return pn.possible || t('wl.input.phone_error', 'Ungültige Telefonnummer');
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
format: (value) => {
|
|
38
|
+
if (!value) return undefined;
|
|
39
|
+
const pn = parsePhoneNumber(value, { regionCode: region });
|
|
40
|
+
return pn.possible && pn.number?.e164 ? pn.number.e164 : value;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const BUILT_IN_PRESETS: Record<string, (t: TranslateFn) => InputPreset> = {
|
|
46
|
+
email: (t) => emailPreset(t),
|
|
47
|
+
phone: (t) => phonePreset(undefined, t),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function resolvePreset(
|
|
51
|
+
preset: string | InputPreset,
|
|
52
|
+
t: TranslateFn = defaultT,
|
|
53
|
+
): InputPreset {
|
|
54
|
+
if (typeof preset === 'string') {
|
|
55
|
+
const factory = BUILT_IN_PRESETS[preset];
|
|
56
|
+
if (!factory) throw new Error(`Unknown preset: "${preset}"`);
|
|
57
|
+
return factory(t);
|
|
58
|
+
}
|
|
59
|
+
return preset;
|
|
60
|
+
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
:aria-invalid="error || undefined"
|
|
32
32
|
:aria-label="ariaLabel || undefined"
|
|
33
33
|
:aria-describedby="ariaDescribedby || undefined"
|
|
34
|
+
:hide-no-data="hideNoData"
|
|
34
35
|
:menu-props="computedMenuProps"
|
|
35
36
|
v-model="internalValue"
|
|
36
37
|
@click:append="onClickAppend"
|
|
@@ -61,6 +62,12 @@
|
|
|
61
62
|
<slot name="append-inner"></slot>
|
|
62
63
|
</template>
|
|
63
64
|
|
|
65
|
+
<template #no-data>
|
|
66
|
+
<slot name="no-data">
|
|
67
|
+
<v-list-item :title="resolvedNoDataText" />
|
|
68
|
+
</slot>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
64
71
|
<template v-if="$slots['item']" #item="slotProps">
|
|
65
72
|
<slot name="item" v-bind="slotProps"></slot>
|
|
66
73
|
</template>
|
|
@@ -76,6 +83,9 @@ import './Select.css';
|
|
|
76
83
|
import { ref, watch, nextTick, computed, inject } from 'vue';
|
|
77
84
|
import { defineProps, defineEmits } from 'vue';
|
|
78
85
|
import { siteColors } from "../../utils/index";
|
|
86
|
+
import { useI18n } from 'vue-i18n';
|
|
87
|
+
|
|
88
|
+
const { t } = useI18n();
|
|
79
89
|
|
|
80
90
|
// Inject theme colors from ThemeProvider, fallback to global siteColors
|
|
81
91
|
const injectedThemeColors = inject('themeColors', null);
|
|
@@ -306,6 +316,25 @@ const props = defineProps({
|
|
|
306
316
|
type: String,
|
|
307
317
|
default: undefined,
|
|
308
318
|
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* When true, hides the dropdown menu when there are no items.
|
|
322
|
+
* Set to false to allow the menu to open and show a "no data" message.
|
|
323
|
+
* Default is false — the menu opens and shows noDataText.
|
|
324
|
+
*/
|
|
325
|
+
hideNoData: {
|
|
326
|
+
type: Boolean,
|
|
327
|
+
default: false,
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Text shown in the dropdown when there are no items and hideNoData is false.
|
|
332
|
+
* Default is 'No data available'.
|
|
333
|
+
*/
|
|
334
|
+
noDataText: {
|
|
335
|
+
type: String,
|
|
336
|
+
default: undefined,
|
|
337
|
+
},
|
|
309
338
|
});
|
|
310
339
|
|
|
311
340
|
// Define events that can be emitted by the component
|
|
@@ -319,6 +348,8 @@ const emits = defineEmits([
|
|
|
319
348
|
'update:search', // Emit search updates for filtering items
|
|
320
349
|
]);
|
|
321
350
|
|
|
351
|
+
const resolvedNoDataText = computed(() => props.noDataText ?? t('wl.select.no_data'));
|
|
352
|
+
|
|
322
353
|
// Define a reactive reference for internal value based on the modelValue prop
|
|
323
354
|
const internalValue = ref(props.modelValue);
|
|
324
355
|
|
package/src/components/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { default as Dialog } from './Dialog/Dialog.vue';
|
|
|
30
30
|
export { default as AnamneseAnswerDialog } from './AnamneseAnswerDialog/AnamneseAnswerDialog.vue';
|
|
31
31
|
export { default as EditField } from './EditField/EditField.vue';
|
|
32
32
|
export { default as Input } from './Input/Input.vue';
|
|
33
|
+
export { emailPreset, phonePreset, type InputPreset } from './Input/presets';
|
|
33
34
|
export { default as Modal } from './Modal/Modal.vue';
|
|
34
35
|
export { default as NotificationBubble } from './NotificationBubble/NotificationBubble.vue';
|
|
35
36
|
export { default as OtpInput } from './OtpInput/OtpInput.vue';
|
package/src/i18n/locales/de.json
CHANGED
package/src/i18n/locales/en.json
CHANGED
package/src/pages/SelectPage.vue
CHANGED
|
@@ -117,6 +117,119 @@
|
|
|
117
117
|
</div>
|
|
118
118
|
</div>
|
|
119
119
|
|
|
120
|
+
<!-- No Data / Empty State -->
|
|
121
|
+
<div class="section">
|
|
122
|
+
<h3 class="section-title">No Data / Empty State</h3>
|
|
123
|
+
<p class="section-description">When items are empty, the dropdown opens and shows a configurable message (hideNoData defaults to false)</p>
|
|
124
|
+
<div class="examples-grid">
|
|
125
|
+
<div class="example-item">
|
|
126
|
+
<h4>Default (No Data Available)</h4>
|
|
127
|
+
<Select
|
|
128
|
+
v-model="noDataDefault"
|
|
129
|
+
label="Select an option"
|
|
130
|
+
:items="[]"
|
|
131
|
+
/>
|
|
132
|
+
<p class="value-display">Empty items — click to see default message</p>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="example-item">
|
|
136
|
+
<h4>Custom No Data Text</h4>
|
|
137
|
+
<Select
|
|
138
|
+
v-model="noDataCustomText"
|
|
139
|
+
label="Select a relationship"
|
|
140
|
+
:items="[]"
|
|
141
|
+
no-data-text="Failed to load relationships"
|
|
142
|
+
/>
|
|
143
|
+
<p class="value-display">Simulates API failure with custom message</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="example-item">
|
|
147
|
+
<h4>Hidden No Data (old behavior)</h4>
|
|
148
|
+
<Select
|
|
149
|
+
v-model="noDataHidden"
|
|
150
|
+
label="Select an option"
|
|
151
|
+
:items="[]"
|
|
152
|
+
:hide-no-data="true"
|
|
153
|
+
/>
|
|
154
|
+
<p class="value-display">hideNoData=true — dropdown won't open</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="example-item">
|
|
158
|
+
<h4>Custom No Data Slot</h4>
|
|
159
|
+
<Select
|
|
160
|
+
v-model="noDataCustomSlot"
|
|
161
|
+
label="Select an option"
|
|
162
|
+
:items="[]"
|
|
163
|
+
>
|
|
164
|
+
<template #no-data>
|
|
165
|
+
<div class="custom-no-data">
|
|
166
|
+
<p class="custom-no-data-title">Nothing here yet</p>
|
|
167
|
+
<p class="custom-no-data-desc">Try a different search or add a new item.</p>
|
|
168
|
+
<button class="custom-no-data-btn" @click="onNoDataAction">Add new item</button>
|
|
169
|
+
</div>
|
|
170
|
+
</template>
|
|
171
|
+
</Select>
|
|
172
|
+
<p class="value-display">Uses #no-data slot for custom content</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- No Data - SelectAutocomplete -->
|
|
178
|
+
<div class="section">
|
|
179
|
+
<h3 class="section-title">No Data — SelectAutocomplete</h3>
|
|
180
|
+
<p class="section-description">Same no-data behavior applies to the autocomplete variant</p>
|
|
181
|
+
<div class="examples-grid">
|
|
182
|
+
<div class="example-item">
|
|
183
|
+
<h4>Default (No Data Available)</h4>
|
|
184
|
+
<SelectAutocomplete
|
|
185
|
+
v-model="noDataAutocompleteDefault"
|
|
186
|
+
label="Search..."
|
|
187
|
+
:items="[]"
|
|
188
|
+
/>
|
|
189
|
+
<p class="value-display">Empty items — type to see default message</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="example-item">
|
|
193
|
+
<h4>Custom No Data Text</h4>
|
|
194
|
+
<SelectAutocomplete
|
|
195
|
+
v-model="noDataAutocompleteCustom"
|
|
196
|
+
label="Search a patient"
|
|
197
|
+
:items="[]"
|
|
198
|
+
no-data-text="Kein Patient gefunden"
|
|
199
|
+
/>
|
|
200
|
+
<p class="value-display">Custom text via noDataText prop</p>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="example-item">
|
|
204
|
+
<h4>Custom No Data Slot</h4>
|
|
205
|
+
<SelectAutocomplete
|
|
206
|
+
v-model="noDataAutocompleteSlot"
|
|
207
|
+
label="Search..."
|
|
208
|
+
:items="[]"
|
|
209
|
+
>
|
|
210
|
+
<template #no-data>
|
|
211
|
+
<div class="custom-no-data">
|
|
212
|
+
<p class="custom-no-data-title">No results found</p>
|
|
213
|
+
<p class="custom-no-data-desc">Try adjusting your search terms.</p>
|
|
214
|
+
</div>
|
|
215
|
+
</template>
|
|
216
|
+
</SelectAutocomplete>
|
|
217
|
+
<p class="value-display">Uses #no-data slot for custom content</p>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div class="example-item">
|
|
221
|
+
<h4>Hidden No Data</h4>
|
|
222
|
+
<SelectAutocomplete
|
|
223
|
+
v-model="noDataAutocompleteHidden"
|
|
224
|
+
label="Search..."
|
|
225
|
+
:items="[]"
|
|
226
|
+
:hide-no-data="true"
|
|
227
|
+
/>
|
|
228
|
+
<p class="value-display">hideNoData=true — menu stays closed</p>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
120
233
|
<!-- Multiple Selection -->
|
|
121
234
|
<div class="section">
|
|
122
235
|
<h3 class="section-title">Multiple Selection</h3>
|
|
@@ -835,6 +948,20 @@ const disabledValue = ref({ title: 'Apple', value: 'apple' });
|
|
|
835
948
|
const errorValue = ref(null);
|
|
836
949
|
const preselectedValue = ref({ title: 'Cherry', value: 'cherry' });
|
|
837
950
|
|
|
951
|
+
// No data / empty state
|
|
952
|
+
const noDataDefault = ref(null);
|
|
953
|
+
const noDataCustomText = ref(null);
|
|
954
|
+
const noDataHidden = ref(null);
|
|
955
|
+
const noDataCustomSlot = ref(null);
|
|
956
|
+
const noDataAutocompleteDefault = ref(null);
|
|
957
|
+
const noDataAutocompleteCustom = ref(null);
|
|
958
|
+
const noDataAutocompleteSlot = ref(null);
|
|
959
|
+
const noDataAutocompleteHidden = ref(null);
|
|
960
|
+
|
|
961
|
+
const onNoDataAction = () => {
|
|
962
|
+
alert('Add new item clicked');
|
|
963
|
+
};
|
|
964
|
+
|
|
838
965
|
// Multiple selection
|
|
839
966
|
const multipleValue = ref([]);
|
|
840
967
|
const chipsValue = ref([]);
|
|
@@ -1299,4 +1426,43 @@ const removeSelection = (index: number) => {
|
|
|
1299
1426
|
.patient-edit-btn:hover {
|
|
1300
1427
|
background-color: rgba(23, 39, 116, 0.05);
|
|
1301
1428
|
}
|
|
1429
|
+
|
|
1430
|
+
/* Custom no-data slot styles */
|
|
1431
|
+
.custom-no-data {
|
|
1432
|
+
display: flex;
|
|
1433
|
+
flex-direction: column;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
padding: 1.25rem 1rem;
|
|
1436
|
+
text-align: center;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.custom-no-data-title {
|
|
1440
|
+
font-size: 0.9375rem;
|
|
1441
|
+
font-weight: 600;
|
|
1442
|
+
color: #333;
|
|
1443
|
+
margin: 0 0 0.375rem 0;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.custom-no-data-desc {
|
|
1447
|
+
font-size: 0.8125rem;
|
|
1448
|
+
color: #888;
|
|
1449
|
+
margin: 0 0 0.75rem 0;
|
|
1450
|
+
line-height: 1.4;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.custom-no-data-btn {
|
|
1454
|
+
background: none;
|
|
1455
|
+
border: 1.5px solid #4caf50;
|
|
1456
|
+
color: #4caf50;
|
|
1457
|
+
border-radius: 999px;
|
|
1458
|
+
padding: 0.375rem 1rem;
|
|
1459
|
+
font-size: 0.8125rem;
|
|
1460
|
+
font-weight: 500;
|
|
1461
|
+
cursor: pointer;
|
|
1462
|
+
transition: background-color 0.2s;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.custom-no-data-btn:hover {
|
|
1466
|
+
background-color: rgba(76, 175, 80, 0.08);
|
|
1467
|
+
}
|
|
1302
1468
|
</style>
|