whelk-ui 0.0.2 → 0.0.3
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/README.md +2 -0
- package/package.json +4 -3
- package/playwright.config.ts +79 -0
- package/src/components/add_object/AddObject.vue +40 -0
- package/src/components/card/CardComponent.vue +14 -0
- package/src/components/card/card_footer/CardFooter.vue +19 -0
- package/src/components/card/card_header/CardHeader.vue +16 -0
- package/src/components/check_box/CheckBox.vue +42 -0
- package/src/components/datetime/DatetimeComponent.vue +147 -0
- package/src/components/drop_down/DropDown.vue +104 -0
- package/src/components/drop_down/drop_down_item/DropDownItem.vue +58 -0
- package/src/components/form_group/FormGroup.spec.ts +16 -0
- package/src/components/form_group/FormGroup.vue +19 -0
- package/src/components/number_input/NumberInput.spec.ts +712 -0
- package/src/components/number_input/NumberInput.vue +264 -0
- package/src/components/password_input/PasswordInput.vue +166 -0
- package/src/components/render_error_message/RenderErrorMessage.spec.ts +0 -0
- package/src/components/render_error_message/RenderErrorMessage.vue +32 -0
- package/src/components/switch/SwitchComponent.vue +152 -0
- package/src/components/text_area/TextArea.vue +151 -0
- package/src/components/text_input/TextInput.vue +178 -0
- package/src/components/tool_tip/ToolTip.vue +96 -0
- package/src/utils/enums/ObjectTitleCaseEnums.ts +17 -0
- package/src/utils/enums/ObjectTypeEnums.ts +15 -0
- package/src/utils/interfaces/DocumentItemInterface.ts +5 -0
- package/src/utils/interfaces/DropDownItemsInterface.ts +8 -0
- package/src/utils/interfaces/FolderItemInterface.ts +4 -0
- package/src/utils/interfaces/MenuItemInterface.ts +10 -0
- package/tests/example.spec.ts +18 -0
- package/vite.config.ts +40 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whelk-ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "vue-tsc -b && vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
|
+
"test:e2e": "playwright test",
|
|
10
11
|
"test:unit": "vitest"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
@@ -14,10 +15,10 @@
|
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"@csstools/postcss-global-data": "^3.1.0",
|
|
18
|
+
"@playwright/test": "^1.58.0",
|
|
17
19
|
"@types/node": "^24.10.1",
|
|
18
20
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
19
|
-
"@vitest/browser-playwright": "^4.0.
|
|
20
|
-
"@vitest/coverage-v8": "^4.0.8",
|
|
21
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
21
22
|
"@vitest/eslint-plugin": "^1.4.0",
|
|
22
23
|
"@vue/tsconfig": "^0.8.1",
|
|
23
24
|
"petite-vue-i18n": "^11.2.8",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read environment variables from file.
|
|
5
|
+
* https://github.com/motdotla/dotenv
|
|
6
|
+
*/
|
|
7
|
+
// import dotenv from 'dotenv';
|
|
8
|
+
// import path from 'path';
|
|
9
|
+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
13
|
+
*/
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
testDir: './tests',
|
|
16
|
+
/* Run tests in files in parallel */
|
|
17
|
+
fullyParallel: true,
|
|
18
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
19
|
+
forbidOnly: !!process.env.CI,
|
|
20
|
+
/* Retry on CI only */
|
|
21
|
+
retries: process.env.CI ? 2 : 0,
|
|
22
|
+
/* Opt out of parallel tests on CI. */
|
|
23
|
+
workers: process.env.CI ? 1 : undefined,
|
|
24
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
25
|
+
reporter: 'html',
|
|
26
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
27
|
+
use: {
|
|
28
|
+
/* Base URL to use in actions like `await page.goto('')`. */
|
|
29
|
+
// baseURL: 'http://localhost:3000',
|
|
30
|
+
|
|
31
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
32
|
+
trace: 'on-first-retry',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/* Configure projects for major browsers */
|
|
36
|
+
projects: [
|
|
37
|
+
{
|
|
38
|
+
name: 'chromium',
|
|
39
|
+
use: { ...devices['Desktop Chrome'] },
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
name: 'firefox',
|
|
44
|
+
use: { ...devices['Desktop Firefox'] },
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
name: 'webkit',
|
|
49
|
+
use: { ...devices['Desktop Safari'] },
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/* Test against mobile viewports. */
|
|
53
|
+
// {
|
|
54
|
+
// name: 'Mobile Chrome',
|
|
55
|
+
// use: { ...devices['Pixel 5'] },
|
|
56
|
+
// },
|
|
57
|
+
// {
|
|
58
|
+
// name: 'Mobile Safari',
|
|
59
|
+
// use: { ...devices['iPhone 12'] },
|
|
60
|
+
// },
|
|
61
|
+
|
|
62
|
+
/* Test against branded browsers. */
|
|
63
|
+
// {
|
|
64
|
+
// name: 'Microsoft Edge',
|
|
65
|
+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
66
|
+
// },
|
|
67
|
+
// {
|
|
68
|
+
// name: 'Google Chrome',
|
|
69
|
+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
70
|
+
// },
|
|
71
|
+
],
|
|
72
|
+
|
|
73
|
+
/* Run your local dev server before starting the tests */
|
|
74
|
+
// webServer: {
|
|
75
|
+
// command: 'npm run start',
|
|
76
|
+
// url: 'http://localhost:3000',
|
|
77
|
+
// reuseExistingServer: !process.env.CI,
|
|
78
|
+
// },
|
|
79
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {ObjectTitleCaseEnums} from "../../utils/enums/ObjectTitleCaseEnums.ts";
|
|
3
|
+
|
|
4
|
+
// DEFINE PROPS
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
objectType: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
validator: function (value: string): boolean {
|
|
10
|
+
const enumValues: string[] = Object.values(ObjectTitleCaseEnums);
|
|
11
|
+
return enumValues.includes(value);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button
|
|
20
|
+
class="add-object"
|
|
21
|
+
type="button"
|
|
22
|
+
>
|
|
23
|
+
+ {{objectType}}
|
|
24
|
+
</button>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.add-object {
|
|
29
|
+
width: 100%;
|
|
30
|
+
border-style: dashed;
|
|
31
|
+
border-width: var(--border-width);
|
|
32
|
+
border-radius: var(--border-radius);
|
|
33
|
+
padding: 0.5rem 0.75rem;
|
|
34
|
+
font-weight: lighter;
|
|
35
|
+
font-size: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<div class="card-footer">
|
|
5
|
+
<slot></slot>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<style scoped>
|
|
10
|
+
.card-footer {
|
|
11
|
+
padding: 0 0 0.75rem 0;
|
|
12
|
+
|
|
13
|
+
@media (--large-screen) {
|
|
14
|
+
margin: 1rem -2rem -1rem -2rem;
|
|
15
|
+
padding: 1rem 2rem;
|
|
16
|
+
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
<template>
|
|
3
|
+
<div class="card-header">
|
|
4
|
+
<slot />
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<style scoped>
|
|
9
|
+
.card-header {
|
|
10
|
+
border-radius: 0;
|
|
11
|
+
|
|
12
|
+
@media (--large-screen) {
|
|
13
|
+
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
// Model
|
|
5
|
+
const model = defineModel();
|
|
6
|
+
|
|
7
|
+
// Setup Props
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
id: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
label: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Computed
|
|
20
|
+
const checkboxId = computed(() => {
|
|
21
|
+
return `checkbox-${props.id}`;
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div class="checkbox-component">
|
|
27
|
+
<input :id="checkboxId" :name="label" v-model="model" type="checkbox" />
|
|
28
|
+
<label :for="checkboxId">{{ props.label }}</label>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
.checkbox-component {
|
|
34
|
+
label {
|
|
35
|
+
padding: 0.5rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
input {
|
|
39
|
+
padding: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import FormGroup from '../form_group/FormGroup.vue';
|
|
3
|
+
import {computed, ref} from 'vue';
|
|
4
|
+
import RenderErrorMessage from '../render_error_message/RenderErrorMessage.vue';
|
|
5
|
+
import ToolTip from '../tool_tip/ToolTip.vue';
|
|
6
|
+
|
|
7
|
+
// Define Emits
|
|
8
|
+
const emit = defineEmits(['isValid']);
|
|
9
|
+
|
|
10
|
+
// Define Props
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
isRequired: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
},
|
|
16
|
+
label: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
tooltipMessage: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: false,
|
|
23
|
+
default: '',
|
|
24
|
+
},
|
|
25
|
+
tooltipTitle: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
default: '',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Define Models
|
|
33
|
+
const model = defineModel('model', {
|
|
34
|
+
type: Date,
|
|
35
|
+
required: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Define ref
|
|
39
|
+
const hasError = ref(false);
|
|
40
|
+
const errorMessage = ref('');
|
|
41
|
+
|
|
42
|
+
// Computed
|
|
43
|
+
const getId = computed(() => {
|
|
44
|
+
// Return an id made up of input- + title
|
|
45
|
+
return 'input-' + props.label?.toLowerCase()?.replace(' ', '-');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function checkValidation() {
|
|
49
|
+
// Fall back to defaults
|
|
50
|
+
hasError.value = false;
|
|
51
|
+
errorMessage.value = '';
|
|
52
|
+
|
|
53
|
+
// Get the length of the model and if NaN fallback to 0
|
|
54
|
+
let modelLength: number = Number(model?.value?.toString().length);
|
|
55
|
+
modelLength = isNaN(modelLength) ? 0 : modelLength;
|
|
56
|
+
|
|
57
|
+
// Check the first "required" condition
|
|
58
|
+
if (props.isRequired && modelLength === 0) {
|
|
59
|
+
hasError.value = true;
|
|
60
|
+
errorMessage.value = 'This field is required';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set the defined ref and tell parent
|
|
64
|
+
emit('isValid', !hasError.value);
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<FormGroup class="datetime-component">
|
|
70
|
+
<label :for="getId">
|
|
71
|
+
<ToolTip
|
|
72
|
+
v-if="props.tooltipMessage !== ''"
|
|
73
|
+
:title="tooltipTitle"
|
|
74
|
+
:message="tooltipMessage"
|
|
75
|
+
:id="getId"
|
|
76
|
+
/>
|
|
77
|
+
{{
|
|
78
|
+
label
|
|
79
|
+
}}<span v-if="isRequired" aria-description="Field is required"
|
|
80
|
+
>*</span
|
|
81
|
+
>
|
|
82
|
+
</label>
|
|
83
|
+
<input
|
|
84
|
+
:id="getId"
|
|
85
|
+
type="datetime-local"
|
|
86
|
+
:name="props.label"
|
|
87
|
+
v-model="model"
|
|
88
|
+
v-on:keyup="checkValidation"
|
|
89
|
+
v-on:focusout="checkValidation"
|
|
90
|
+
/>
|
|
91
|
+
<RenderErrorMessage :error-message="errorMessage"/>
|
|
92
|
+
</FormGroup>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.datetime-component {
|
|
97
|
+
|
|
98
|
+
label {
|
|
99
|
+
margin-bottom: 6px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
span {
|
|
103
|
+
color: var(--text-red);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
input {
|
|
107
|
+
border-style: var(--border-style);
|
|
108
|
+
border-width: var(--border-width);
|
|
109
|
+
border-radius: var(--border-radius);
|
|
110
|
+
border-color: var(--border);
|
|
111
|
+
box-sizing: border-box;
|
|
112
|
+
-moz-box-sizing: border-box;
|
|
113
|
+
-webkit-box-sizing: border-box;
|
|
114
|
+
|
|
115
|
+
&:focus {
|
|
116
|
+
border-color: var(--secondary);
|
|
117
|
+
border-width: 2px;
|
|
118
|
+
outline: none;
|
|
119
|
+
padding: calc(0.5rem - 1px);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&.compact {
|
|
124
|
+
> label {
|
|
125
|
+
font-size: 1rem;
|
|
126
|
+
line-height: 1.25rem;
|
|
127
|
+
margin-bottom: 2px;
|
|
128
|
+
|
|
129
|
+
@media (--large-screen) {
|
|
130
|
+
font-size: 0.75rem;
|
|
131
|
+
line-height: 1rem;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
> input {
|
|
136
|
+
font-size: 1.25rem;
|
|
137
|
+
line-height: 1.5rem;
|
|
138
|
+
padding: 0.25rem;
|
|
139
|
+
|
|
140
|
+
@media (--large-screen) {
|
|
141
|
+
font-size: 1rem;
|
|
142
|
+
line-height: 1.25rem;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {ref} from 'vue';
|
|
3
|
+
import type {PropType} from "vue";
|
|
4
|
+
import type {DropDownItemsInterface} from "../../utils/interfaces/DropDownItemsInterface.ts";
|
|
5
|
+
import DropDownItem from "./drop_down_item/DropDownItem.vue";
|
|
6
|
+
|
|
7
|
+
// Define props
|
|
8
|
+
defineProps({
|
|
9
|
+
dropDownItems: {
|
|
10
|
+
type: Array as PropType<DropDownItemsInterface[]>,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Define emits
|
|
16
|
+
const emits = defineEmits(["dropDownItemClicked"]);
|
|
17
|
+
|
|
18
|
+
// Define refs
|
|
19
|
+
const menuOpen = ref(false);
|
|
20
|
+
|
|
21
|
+
// Define methods
|
|
22
|
+
function dropDownMenuClicked() {
|
|
23
|
+
menuOpen.value = !menuOpen.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dropDownItemClicked(trigger: string) {
|
|
27
|
+
// Emit upstream
|
|
28
|
+
emits("dropDownItemClicked", trigger);
|
|
29
|
+
|
|
30
|
+
// Close the menu
|
|
31
|
+
menuOpen.value = false;
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<div class="drop-down">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
v-on:click="dropDownMenuClicked"
|
|
40
|
+
>
|
|
41
|
+
<slot />
|
|
42
|
+
</button>
|
|
43
|
+
<Transition>
|
|
44
|
+
<DropDownItem
|
|
45
|
+
v-show="menuOpen"
|
|
46
|
+
v-on:dropDownItemClicked="dropDownItemClicked"
|
|
47
|
+
:drop-down-items="dropDownItems"
|
|
48
|
+
/>
|
|
49
|
+
</Transition>
|
|
50
|
+
<Transition>
|
|
51
|
+
<div
|
|
52
|
+
v-on:click="dropDownMenuClicked"
|
|
53
|
+
v-if="menuOpen"
|
|
54
|
+
class="drop-down-backdrop"
|
|
55
|
+
></div>
|
|
56
|
+
</Transition>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style scoped>
|
|
61
|
+
.drop-down {
|
|
62
|
+
> button {
|
|
63
|
+
border: solid;
|
|
64
|
+
border-width: 1px;
|
|
65
|
+
border-radius: var(--border-radius);
|
|
66
|
+
border-color: var(--border-muted);
|
|
67
|
+
background: var(--bg-light);
|
|
68
|
+
box-shadow: none;
|
|
69
|
+
font-size: 1rem;
|
|
70
|
+
line-height: 1.25rem;
|
|
71
|
+
height: 2rem;
|
|
72
|
+
padding: 0 1rem;
|
|
73
|
+
position: relative;
|
|
74
|
+
z-index: 10;
|
|
75
|
+
|
|
76
|
+
> svg {
|
|
77
|
+
width: 1rem;
|
|
78
|
+
height: 1rem;
|
|
79
|
+
transform: translateY(2px);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.drop-down-backdrop {
|
|
84
|
+
width: 100vw;
|
|
85
|
+
height: 100dvh;
|
|
86
|
+
z-index: 5;
|
|
87
|
+
background-color: hsla(0, 0%, 0%, 0.7);
|
|
88
|
+
position: fixed;
|
|
89
|
+
top: 0;
|
|
90
|
+
left: 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.v-enter-active,
|
|
95
|
+
.v-leave-active {
|
|
96
|
+
transition: opacity 0.5s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.v-enter-from,
|
|
100
|
+
.v-leave-to {
|
|
101
|
+
opacity: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import type {DropDownItemsInterface} from "../../../utils/interfaces/DropDownItemsInterface.ts";
|
|
4
|
+
|
|
5
|
+
// Define props
|
|
6
|
+
defineProps({
|
|
7
|
+
dropDownItems: {
|
|
8
|
+
type: Array as PropType<DropDownItemsInterface[]>,
|
|
9
|
+
required: true,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Define emits
|
|
14
|
+
const emits = defineEmits(["dropDownItemClicked"]);
|
|
15
|
+
|
|
16
|
+
// Define methods
|
|
17
|
+
function dropDownItemClicked(trigger: string) {
|
|
18
|
+
emits("dropDownItemClicked", trigger);
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="drop-down-items">
|
|
24
|
+
<div class="drop-down-item"
|
|
25
|
+
v-for="item in dropDownItems"
|
|
26
|
+
v-on:click="dropDownItemClicked(item.trigger)"
|
|
27
|
+
:key="item.label"
|
|
28
|
+
>
|
|
29
|
+
<component :is="item.icon" />
|
|
30
|
+
{{item.label}}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<style scoped>
|
|
36
|
+
.drop-down-items {
|
|
37
|
+
position: absolute;
|
|
38
|
+
background-color: var(--bg-light);
|
|
39
|
+
border: solid;
|
|
40
|
+
border-width: var(--border-width);
|
|
41
|
+
border-radius: var(--border-radius);
|
|
42
|
+
border-color: var(--border-muted);
|
|
43
|
+
z-index: 20;
|
|
44
|
+
|
|
45
|
+
> .drop-down-item {
|
|
46
|
+
padding: 0.25rem 0.5rem;
|
|
47
|
+
|
|
48
|
+
&:hover {
|
|
49
|
+
background-color: var(--bg-dark);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
> svg {
|
|
53
|
+
width: 0.75rem;
|
|
54
|
+
height: 0.75rem;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// FormGroup.spec.ts
|
|
2
|
+
import { describe, test, expect } from "vitest";
|
|
3
|
+
import FormGroup from "./FormGroup.vue";
|
|
4
|
+
import { mount } from "@vue/test-utils";
|
|
5
|
+
|
|
6
|
+
describe("FormGroup", async () => {
|
|
7
|
+
test("form group slot renders main content", () => {
|
|
8
|
+
const wrapper = mount(FormGroup, {
|
|
9
|
+
slots: {
|
|
10
|
+
default: "Main Content",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
expect(wrapper.html()).toContain("Main Content");
|
|
14
|
+
expect(wrapper.find(".form-group").text()).toContain("Main Content");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<div class="form-group">
|
|
5
|
+
<slot></slot>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<style scoped>
|
|
10
|
+
.form-group {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
margin-bottom: 1.75rem;
|
|
14
|
+
|
|
15
|
+
&.compact {
|
|
16
|
+
margin-bottom: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</style>
|