srcdev-nuxt-forms 2.1.28 → 2.1.30
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/assets/styles/forms/themes/_primary.css +10 -0
- package/components/forms/input-checkbox/{variants/MultipleCheckboxes.vue → MultipleCheckboxes.vue} +7 -3
- package/components/forms/input-checkbox/{variants/SingleCheckbox.vue → SingleCheckbox.vue} +7 -3
- package/components/forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +98 -0
- package/components/forms/input-checkbox/tests/data/tags.json +67 -0
- package/components/forms/input-checkbox-radio/InputCheckboxRadioCore.vue +3 -3
- package/components/forms/input-radio/MultipleRadiobuttons.vue +6 -2
- package/components/forms/input-radio/tests/MultipleRadioButtons.spec.ts +89 -0
- package/components/forms/input-radio/tests/data/tags.json +67 -0
- package/components/forms/toggle-switch/ToggleSwitchCore.vue +217 -0
- package/components/forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +88 -0
- package/package.json +16 -13
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
|
|
28
28
|
--theme-form-range-accent-color: light-dark(var(--blue-12), var(--blue-));
|
|
29
29
|
|
|
30
|
+
/*
|
|
31
|
+
* ToggleSwitch
|
|
32
|
+
**/
|
|
33
|
+
--theme-form-toggle-bg-off: light-dark(var(--gray-1), var(--gray-4));
|
|
34
|
+
--theme-form-toggle-bg-on: light-dark(var(--gray-1), var(--gray-6));
|
|
35
|
+
--theme-form-toggle-border: 1px solid light-dark(var(--blue-12), var(--blue-1));
|
|
36
|
+
--theme-form-toggle-outline: 1px solid light-dark(var(--blue-12), var(--blue-12));
|
|
37
|
+
--theme-form-toggle-symbol-off: var(--orange-12);
|
|
38
|
+
--theme-form-toggle-symbol-on: var(--gray-12);
|
|
39
|
+
|
|
30
40
|
/*
|
|
31
41
|
* Checkbox as button
|
|
32
42
|
**/
|
package/components/forms/input-checkbox/{variants/MultipleCheckboxes.vue → MultipleCheckboxes.vue}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<fieldset class="multiple-checkboxes-fieldset" :class="[{ error: fieldHasError }]">
|
|
2
|
+
<fieldset class="multiple-checkboxes-fieldset" :class="[elementClasses, { error: fieldHasError }]" :data-testid>
|
|
3
3
|
<legend :class="[{ 'has-description': hasDescription }]">{{ legend }}</legend>
|
|
4
4
|
|
|
5
5
|
<div v-if="hasDescriptionSlot" :id="`${id}-description`">
|
|
@@ -59,10 +59,14 @@
|
|
|
59
59
|
</template>
|
|
60
60
|
|
|
61
61
|
<script setup lang="ts">
|
|
62
|
-
import propValidators from '
|
|
62
|
+
import propValidators from '../c12/prop-validators';
|
|
63
63
|
import type { IOptionsConfig, IFormMultipleOptions } from '@/types/types.forms';
|
|
64
64
|
|
|
65
|
-
const { id, name, legend, label, required, fieldHasError, placeholder, isButton, errorMessage, size, optionsLayout, equalCols, styleClassPassthrough, theme, direction } = defineProps({
|
|
65
|
+
const { dataTestid, id, name, legend, label, required, fieldHasError, placeholder, isButton, errorMessage, size, optionsLayout, equalCols, styleClassPassthrough, theme, direction } = defineProps({
|
|
66
|
+
dataTestid: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'multiple-checkboxes',
|
|
69
|
+
},
|
|
66
70
|
id: {
|
|
67
71
|
type: String,
|
|
68
72
|
required: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<fieldset class="single-checkbox-fieldset" :class="[{ error: fieldHasError }]">
|
|
2
|
+
<fieldset class="single-checkbox-fieldset" :class="[elementClasses, { error: fieldHasError }]" :data-testid>
|
|
3
3
|
<legend :class="[{ 'has-description': hasDescription }]">{{ legend }}</legend>
|
|
4
4
|
|
|
5
5
|
<div v-if="hasDescriptionSlot" :id="`${name}-description`">
|
|
@@ -21,10 +21,14 @@
|
|
|
21
21
|
</template>
|
|
22
22
|
|
|
23
23
|
<script setup lang="ts">
|
|
24
|
-
import propValidators from '
|
|
24
|
+
import propValidators from '../c12/prop-validators';
|
|
25
25
|
import type { IFormMultipleOptions } from '@/types/types.forms';
|
|
26
26
|
|
|
27
|
-
const { id, name, legend, label, required, fieldHasError, errorMessage, size, optionsLayout, equalCols, trueValue, falseValue, styleClassPassthrough, theme } = defineProps({
|
|
27
|
+
const { dataTestid, id, name, legend, label, required, fieldHasError, errorMessage, size, optionsLayout, equalCols, trueValue, falseValue, styleClassPassthrough, theme } = defineProps({
|
|
28
|
+
dataTestid: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: 'multiple-radio-buttons',
|
|
31
|
+
},
|
|
28
32
|
id: {
|
|
29
33
|
type: String,
|
|
30
34
|
required: true,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { VueWrapper } from '@vue/test-utils';
|
|
4
|
+
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
|
5
|
+
import ComponentUnderTest from '../MultipleCheckboxes.vue';
|
|
6
|
+
import tagsData from './data/tags.json';
|
|
7
|
+
|
|
8
|
+
let initialPropsData = {
|
|
9
|
+
dataTestid: 'multiple-checkboxes',
|
|
10
|
+
id: 'tags',
|
|
11
|
+
name: 'tags',
|
|
12
|
+
legend: 'Choose tags (as checkboxes)',
|
|
13
|
+
required: true,
|
|
14
|
+
label: 'Check between 3 and 8 tags',
|
|
15
|
+
placeholder: 'eg. Type something here',
|
|
16
|
+
isButton: true,
|
|
17
|
+
errorMessage: 'Please select between 3 and 8 tags',
|
|
18
|
+
fieldHasError: false,
|
|
19
|
+
fieldData: tagsData,
|
|
20
|
+
size: 'small',
|
|
21
|
+
optionsLayout: 'inline',
|
|
22
|
+
styleClassPassthrough: ['testClass'],
|
|
23
|
+
theme: 'primary',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const initialSlots = {
|
|
27
|
+
checkedIcon: () => ``,
|
|
28
|
+
itemIcon: () => `<Icon name="material-symbols:add-2" class="icon" />`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
32
|
+
const wrapperFactory = (propsData = {}, slotsData = {}) => {
|
|
33
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
34
|
+
const mockSlotsData = { ...initialSlots, ...slotsData };
|
|
35
|
+
|
|
36
|
+
return mountSuspended(ComponentUnderTest, {
|
|
37
|
+
attachTo: document.body,
|
|
38
|
+
props: mockPropsData,
|
|
39
|
+
slots: mockSlotsData,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('MultipleCheckboxes Component', () => {
|
|
44
|
+
it('Mounts', async () => {
|
|
45
|
+
wrapper = await wrapperFactory();
|
|
46
|
+
expect(wrapper).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders properly', async () => {
|
|
50
|
+
wrapper = await wrapperFactory();
|
|
51
|
+
const dataTestIdElem = wrapper.attributes('data-testid');
|
|
52
|
+
expect(dataTestIdElem).toBe(initialPropsData.dataTestid);
|
|
53
|
+
expect(wrapper.find('[data-testid]').classes()).toContain('testClass');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('updates checkbox modelValue when items clicked', async () => {
|
|
57
|
+
const modelValue = ref<string[]>([]);
|
|
58
|
+
const propsData = {
|
|
59
|
+
modelValue,
|
|
60
|
+
};
|
|
61
|
+
wrapper = await wrapperFactory(propsData);
|
|
62
|
+
const checkboxElements = wrapper.findAll('input[type="checkbox"]');
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* Test the first checkbox checked
|
|
66
|
+
**/
|
|
67
|
+
const firstCheckbox = checkboxElements[0];
|
|
68
|
+
expect(firstCheckbox.attributes('aria-checked')).toBe('false');
|
|
69
|
+
const firstCheckboxTrueValue = firstCheckbox.attributes('true-value');
|
|
70
|
+
|
|
71
|
+
await firstCheckbox.trigger('click');
|
|
72
|
+
|
|
73
|
+
expect(wrapper.emitted()).toHaveProperty('update:modelValue');
|
|
74
|
+
const firstEmittedEvents = wrapper.emitted('update:modelValue');
|
|
75
|
+
if (firstEmittedEvents && firstEmittedEvents[0]) {
|
|
76
|
+
expect(firstEmittedEvents[0]).includes(firstCheckboxTrueValue);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(firstCheckbox.attributes('aria-checked')).toBe('true');
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* Test the second checkbox chekced
|
|
83
|
+
**/
|
|
84
|
+
const secondCheckbox = checkboxElements[1];
|
|
85
|
+
expect(secondCheckbox.attributes('aria-checked')).toBe('false');
|
|
86
|
+
const secondCheckboxTrueValue = secondCheckbox.attributes('true-value');
|
|
87
|
+
|
|
88
|
+
await secondCheckbox.trigger('click');
|
|
89
|
+
|
|
90
|
+
expect(wrapper.emitted()).toHaveProperty('update:modelValue');
|
|
91
|
+
const secondEmittedEvents = wrapper.emitted('update:modelValue');
|
|
92
|
+
if (secondEmittedEvents && secondEmittedEvents[0]) {
|
|
93
|
+
expect(secondEmittedEvents[1]).includes(secondCheckboxTrueValue);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
expect(secondCheckbox.attributes('aria-checked')).toBe('true');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [
|
|
3
|
+
{
|
|
4
|
+
"id": "pizza",
|
|
5
|
+
"name": "tags",
|
|
6
|
+
"value": "pizza",
|
|
7
|
+
"label": "Pizza"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "italian",
|
|
11
|
+
"name": "tags",
|
|
12
|
+
"value": "italian",
|
|
13
|
+
"label": "Italian"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "vegetarian",
|
|
17
|
+
"name": "tags",
|
|
18
|
+
"value": "vegetarian",
|
|
19
|
+
"label": "Vegetarian"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "stir-fry",
|
|
23
|
+
"name": "tags",
|
|
24
|
+
"value": "stir-fry",
|
|
25
|
+
"label": "Stir-fry"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "asian",
|
|
29
|
+
"name": "tags",
|
|
30
|
+
"value": "asian",
|
|
31
|
+
"label": "Asian"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "cookies",
|
|
35
|
+
"name": "tags",
|
|
36
|
+
"value": "cookies",
|
|
37
|
+
"label": "Cookies"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "dessert",
|
|
41
|
+
"name": "tags",
|
|
42
|
+
"value": "dessert",
|
|
43
|
+
"label": "Dessert"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "baking",
|
|
47
|
+
"name": "tags",
|
|
48
|
+
"value": "baking",
|
|
49
|
+
"label": "Baking"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "pasta",
|
|
53
|
+
"name": "tags",
|
|
54
|
+
"value": "pasta",
|
|
55
|
+
"label": "Pasta"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "chicken",
|
|
59
|
+
"name": "tags",
|
|
60
|
+
"value": "chicken",
|
|
61
|
+
"label": "Chicken"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"total": 5,
|
|
65
|
+
"skip": 0,
|
|
66
|
+
"limit": 10
|
|
67
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<input
|
|
8
8
|
:type
|
|
9
|
-
:true-value
|
|
10
|
-
:false-value
|
|
9
|
+
:true-value
|
|
10
|
+
:false-value
|
|
11
11
|
:id
|
|
12
12
|
:name
|
|
13
13
|
:required="required && !multipleOptions"
|
|
@@ -103,7 +103,7 @@ const isArray = Array.isArray(modelValue.value);
|
|
|
103
103
|
|
|
104
104
|
const isChecked = computed(() => {
|
|
105
105
|
if (isArray) {
|
|
106
|
-
return modelValue.value.
|
|
106
|
+
return modelValue.value.includes(trueValue);
|
|
107
107
|
} else {
|
|
108
108
|
return modelValue.value === trueValue;
|
|
109
109
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<fieldset :aria-required="required" :aria-invalid="fieldHasError" role="radiogroup" class="multiple-radiobuttons-fieldset" :class="[{ error: fieldHasError }]">
|
|
2
|
+
<fieldset :aria-required="required" :aria-invalid="fieldHasError" role="radiogroup" class="multiple-radiobuttons-fieldset" :class="[elementClasses, { error: fieldHasError }]" :data-testid>
|
|
3
3
|
<legend :class="[{ 'has-description': hasDescriptionSlot }]">{{ legend }}</legend>
|
|
4
4
|
|
|
5
5
|
<div v-if="hasDescriptionSlot" :id="`${name}-description`">
|
|
@@ -62,7 +62,11 @@
|
|
|
62
62
|
import propValidators from '../c12/prop-validators';
|
|
63
63
|
import type { IFormMultipleOptions } from '@/types/types.forms';
|
|
64
64
|
|
|
65
|
-
const { id, name, legend, label, required, fieldHasError, placeholder, isButton, errorMessage, size, optionsLayout, equalCols, styleClassPassthrough, theme, direction } = defineProps({
|
|
65
|
+
const { dataTestid, id, name, legend, label, required, fieldHasError, placeholder, isButton, errorMessage, size, optionsLayout, equalCols, styleClassPassthrough, theme, direction } = defineProps({
|
|
66
|
+
dataTestid: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'multiple-radio-buttons',
|
|
69
|
+
},
|
|
66
70
|
id: {
|
|
67
71
|
type: String,
|
|
68
72
|
required: true,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { VueWrapper } from '@vue/test-utils';
|
|
4
|
+
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
|
5
|
+
import ComponentUnderTest from '../MultipleRadioButtons.vue';
|
|
6
|
+
import tagsData from './data/tags.json';
|
|
7
|
+
|
|
8
|
+
let initialPropsData = {
|
|
9
|
+
dataTestid: 'multiple-radio-buttons',
|
|
10
|
+
id: 'tags',
|
|
11
|
+
name: 'tags',
|
|
12
|
+
legend: 'Choose tags (as checkboxes)',
|
|
13
|
+
required: true,
|
|
14
|
+
label: 'Check between 3 and 8 tags',
|
|
15
|
+
placeholder: 'eg. Type something here',
|
|
16
|
+
isButton: true,
|
|
17
|
+
errorMessage: 'Please select between 3 and 8 tags',
|
|
18
|
+
fieldHasError: false,
|
|
19
|
+
fieldData: tagsData,
|
|
20
|
+
size: 'small',
|
|
21
|
+
optionsLayout: 'inline',
|
|
22
|
+
styleClassPassthrough: ['testClass'],
|
|
23
|
+
theme: 'primary',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const initialSlots = {
|
|
27
|
+
checkedIcon: () => ``,
|
|
28
|
+
itemIcon: () => `<Icon name="material-symbols:add-2" class="icon" />`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
32
|
+
const wrapperFactory = (propsData = {}, slotsData = {}) => {
|
|
33
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
34
|
+
const mockSlotsData = { ...initialSlots, ...slotsData };
|
|
35
|
+
|
|
36
|
+
return mountSuspended(ComponentUnderTest, {
|
|
37
|
+
attachTo: document.body,
|
|
38
|
+
props: mockPropsData,
|
|
39
|
+
slots: mockSlotsData,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('MultipleRadioButtons Component', () => {
|
|
44
|
+
it('Mounts', async () => {
|
|
45
|
+
wrapper = await wrapperFactory();
|
|
46
|
+
expect(wrapper).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders properly', async () => {
|
|
50
|
+
wrapper = await wrapperFactory();
|
|
51
|
+
const dataTestIdElem = wrapper.attributes('data-testid');
|
|
52
|
+
expect(dataTestIdElem).toBe(initialPropsData.dataTestid);
|
|
53
|
+
expect(wrapper.find('[data-testid]').classes()).toContain('testClass');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('updates radio modelValue when items clicked', async () => {
|
|
57
|
+
const modelValue = ref<string>('');
|
|
58
|
+
const propsData = {
|
|
59
|
+
modelValue,
|
|
60
|
+
};
|
|
61
|
+
wrapper = await wrapperFactory(propsData);
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Test the first radio button
|
|
65
|
+
**/
|
|
66
|
+
const radiobuttonElements = wrapper.findAll('input[type="radio"]');
|
|
67
|
+
const firstRadioButton = radiobuttonElements[0];
|
|
68
|
+
expect(firstRadioButton.attributes('aria-checked')).toBe('false');
|
|
69
|
+
const firstRadioButtonTrueValue = firstRadioButton.attributes('true-value');
|
|
70
|
+
|
|
71
|
+
await firstRadioButton.trigger('click');
|
|
72
|
+
wrapper.vm.modelValue.value = firstRadioButtonTrueValue;
|
|
73
|
+
expect(firstRadioButton.attributes('aria-checked')).toBe('true');
|
|
74
|
+
expect(wrapper.vm.modelValue.value).toEqual(firstRadioButtonTrueValue);
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Test the second radio button
|
|
78
|
+
**/
|
|
79
|
+
const secondRadioButton = radiobuttonElements[1];
|
|
80
|
+
expect(secondRadioButton.attributes('aria-checked')).toBe('false');
|
|
81
|
+
const secondRadioButtonTrueValue = secondRadioButton.attributes('true-value');
|
|
82
|
+
|
|
83
|
+
await secondRadioButton.trigger('click');
|
|
84
|
+
wrapper.vm.modelValue.value = secondRadioButtonTrueValue;
|
|
85
|
+
expect(firstRadioButton.attributes('aria-checked')).toBe('false');
|
|
86
|
+
expect(secondRadioButton.attributes('aria-checked')).toBe('true');
|
|
87
|
+
expect(wrapper.vm.modelValue.value).toEqual(secondRadioButtonTrueValue);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [
|
|
3
|
+
{
|
|
4
|
+
"id": "pizza",
|
|
5
|
+
"name": "tags",
|
|
6
|
+
"value": "pizza",
|
|
7
|
+
"label": "Pizza"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "italian",
|
|
11
|
+
"name": "tags",
|
|
12
|
+
"value": "italian",
|
|
13
|
+
"label": "Italian"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "vegetarian",
|
|
17
|
+
"name": "tags",
|
|
18
|
+
"value": "vegetarian",
|
|
19
|
+
"label": "Vegetarian"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "stir-fry",
|
|
23
|
+
"name": "tags",
|
|
24
|
+
"value": "stir-fry",
|
|
25
|
+
"label": "Stir-fry"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "asian",
|
|
29
|
+
"name": "tags",
|
|
30
|
+
"value": "asian",
|
|
31
|
+
"label": "Asian"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "cookies",
|
|
35
|
+
"name": "tags",
|
|
36
|
+
"value": "cookies",
|
|
37
|
+
"label": "Cookies"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "dessert",
|
|
41
|
+
"name": "tags",
|
|
42
|
+
"value": "dessert",
|
|
43
|
+
"label": "Dessert"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "baking",
|
|
47
|
+
"name": "tags",
|
|
48
|
+
"value": "baking",
|
|
49
|
+
"label": "Baking"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "pasta",
|
|
53
|
+
"name": "tags",
|
|
54
|
+
"value": "pasta",
|
|
55
|
+
"label": "Pasta"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "chicken",
|
|
59
|
+
"name": "tags",
|
|
60
|
+
"value": "chicken",
|
|
61
|
+
"label": "Chicken"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"total": 5,
|
|
65
|
+
"skip": 0,
|
|
66
|
+
"limit": 10
|
|
67
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="toggle-switch-core" :class="(elementClasses, size)" :data-form-theme="formTheme">
|
|
3
|
+
<label class="toggle-switch-input" :class="[{ round }]" :for="inputId">
|
|
4
|
+
<input type="checkbox" v-model="modelValue" :id="inputId" :aria-describedby="`${id}-description`" :name :required />
|
|
5
|
+
<div class="symbol-wrapper" :class="[{ round }]">
|
|
6
|
+
<div class="symbol" :class="[{ round }]">
|
|
7
|
+
<div v-if="hasIconOnSlot" class="symbol-icon icon-on">
|
|
8
|
+
<slot name="iconOn"></slot>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div v-if="hasIconOffSlot" class="symbol-icon icon-off">
|
|
12
|
+
<slot name="iconOff"></slot>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</label>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import propValidators from '../c12/prop-validators';
|
|
22
|
+
|
|
23
|
+
const { id, name, required, fieldHasError, styleClassPassthrough, theme, round, size } = defineProps({
|
|
24
|
+
id: {
|
|
25
|
+
type: String,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
name: {
|
|
29
|
+
type: String,
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
required: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
fieldHasError: {
|
|
37
|
+
type: Boolean,
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
styleClassPassthrough: {
|
|
41
|
+
type: Array as PropType<string[]>,
|
|
42
|
+
default: () => [],
|
|
43
|
+
},
|
|
44
|
+
theme: {
|
|
45
|
+
type: String as PropType<string>,
|
|
46
|
+
default: 'primary',
|
|
47
|
+
validator(value: string) {
|
|
48
|
+
return propValidators.theme.includes(value);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
round: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: true,
|
|
54
|
+
},
|
|
55
|
+
size: {
|
|
56
|
+
type: String as PropType<string>,
|
|
57
|
+
default: 'normal',
|
|
58
|
+
validator(value: string) {
|
|
59
|
+
return propValidators.size.includes(value);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const slots = useSlots();
|
|
65
|
+
const hasDescriptionSlot = computed(() => slots.description !== undefined);
|
|
66
|
+
const hasIconOnSlot = computed(() => slots.iconOn !== undefined);
|
|
67
|
+
const hasIconOffSlot = computed(() => slots.iconOff !== undefined);
|
|
68
|
+
|
|
69
|
+
const formTheme = computed(() => {
|
|
70
|
+
return fieldHasError ? 'error' : theme;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const modelValue = defineModel();
|
|
74
|
+
const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
|
|
75
|
+
|
|
76
|
+
const inputId = computed(() => `toggle-sitch-${id}`);
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<style lang="css">
|
|
80
|
+
.toggle-switch-core {
|
|
81
|
+
--_transition-duration: 0.4s;
|
|
82
|
+
|
|
83
|
+
.toggle-switch-label {
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.toggle-switch-input {
|
|
88
|
+
position: relative;
|
|
89
|
+
display: inline-block;
|
|
90
|
+
|
|
91
|
+
input {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
width: 0;
|
|
94
|
+
height: 0;
|
|
95
|
+
}
|
|
96
|
+
input:checked {
|
|
97
|
+
--_icon-on-opacity: 1;
|
|
98
|
+
--_icon-off-opacity: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The symbol */
|
|
102
|
+
.symbol-wrapper {
|
|
103
|
+
position: absolute;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
top: 0;
|
|
106
|
+
left: 0;
|
|
107
|
+
right: 0;
|
|
108
|
+
bottom: 0;
|
|
109
|
+
overflow: clip;
|
|
110
|
+
|
|
111
|
+
.symbol {
|
|
112
|
+
display: grid;
|
|
113
|
+
grid-template-areas: 'icon-stack';
|
|
114
|
+
overflow: clip;
|
|
115
|
+
position: absolute;
|
|
116
|
+
|
|
117
|
+
.symbol-icon {
|
|
118
|
+
grid-area: icon-stack;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* ToggleSwitch configurable
|
|
130
|
+
**/
|
|
131
|
+
.toggle-switch-core {
|
|
132
|
+
/* Sizes */
|
|
133
|
+
&.x-small {
|
|
134
|
+
--_symbol-size: 20px;
|
|
135
|
+
}
|
|
136
|
+
&.small {
|
|
137
|
+
--_symbol-size: 24px;
|
|
138
|
+
}
|
|
139
|
+
&.normal {
|
|
140
|
+
--_symbol-size: 34px;
|
|
141
|
+
}
|
|
142
|
+
&.medium {
|
|
143
|
+
--_symbol-size: 40px;
|
|
144
|
+
}
|
|
145
|
+
&.large {
|
|
146
|
+
--_symbol-size: 44px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* .toggle-switch-label {
|
|
150
|
+
} */
|
|
151
|
+
|
|
152
|
+
.toggle-switch-input {
|
|
153
|
+
border: var(--theme-form-toggle-border);
|
|
154
|
+
outline: var(--theme-form-toggle-outline);
|
|
155
|
+
width: calc(var(--_symbol-size) * 2 - 4px);
|
|
156
|
+
height: calc(var(--_symbol-size) + 4px);
|
|
157
|
+
|
|
158
|
+
&.round {
|
|
159
|
+
border-radius: calc(var(--_symbol-size) + 2px);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.symbol-wrapper {
|
|
163
|
+
background-color: var(--theme-form-toggle-bg-off);
|
|
164
|
+
transition: 0.4s;
|
|
165
|
+
|
|
166
|
+
&.round {
|
|
167
|
+
border-radius: var(--_symbol-size);
|
|
168
|
+
}
|
|
169
|
+
.symbol {
|
|
170
|
+
height: calc(var(--_symbol-size) - 6px);
|
|
171
|
+
width: calc(var(--_symbol-size) - 6px);
|
|
172
|
+
left: 4px;
|
|
173
|
+
bottom: 4px;
|
|
174
|
+
background-color: var(--theme-form-toggle-symbol-off);
|
|
175
|
+
transition: var(--_transition-duration);
|
|
176
|
+
|
|
177
|
+
&.round {
|
|
178
|
+
border-radius: 50%;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.symbol-icon {
|
|
182
|
+
transition: opacity var(--_transition-duration);
|
|
183
|
+
|
|
184
|
+
&.icon-on {
|
|
185
|
+
opacity: 0;
|
|
186
|
+
}
|
|
187
|
+
&.icon-off {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* input:checked + .symbol-wrapper .symbol {
|
|
195
|
+
background-color: var(--theme-form-toggle-bg-on);
|
|
196
|
+
} */
|
|
197
|
+
|
|
198
|
+
input:focus-visible + .symbol-wrapper {
|
|
199
|
+
box-shadow: var(--theme-form-focus-box-shadow);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
input:checked + .symbol-wrapper .symbol {
|
|
203
|
+
background-color: var(--theme-form-toggle-symbol-on);
|
|
204
|
+
transform: translateX(26px);
|
|
205
|
+
|
|
206
|
+
.symbol-icon {
|
|
207
|
+
&.icon-on {
|
|
208
|
+
opacity: 1;
|
|
209
|
+
}
|
|
210
|
+
&.icon-off {
|
|
211
|
+
opacity: 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="toggle-switch-with-label" :class="(elementClasses, size)" :data-form-theme="formTheme">
|
|
3
|
+
<p class="toggle-switch-label input-text-label body-normal-bold" :for="`toggle-sitch-${id}`">{{ label }}</p>
|
|
4
|
+
<div v-if="hasDescriptionSlot" :id="`${id}-description`">
|
|
5
|
+
<slot name="description"></slot>
|
|
6
|
+
</div>
|
|
7
|
+
<ToggleSwitchCore v-model="modelValue" :id :name :required :field-has-error :theme :round :size>
|
|
8
|
+
<template v-if="hasIconOnSlot" #iconOn>
|
|
9
|
+
<slot name="iconOn"></slot>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<template v-if="hasIconOffSlot" #iconOff>
|
|
13
|
+
<slot name="iconOff"></slot>
|
|
14
|
+
</template>
|
|
15
|
+
</ToggleSwitchCore>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import propValidators from '../../c12/prop-validators';
|
|
21
|
+
|
|
22
|
+
const { id, name, label, required, fieldHasError, styleClassPassthrough, theme, round, size } = defineProps({
|
|
23
|
+
id: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
name: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
label: {
|
|
32
|
+
type: String,
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
required: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
default: false,
|
|
38
|
+
},
|
|
39
|
+
fieldHasError: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: false,
|
|
42
|
+
},
|
|
43
|
+
styleClassPassthrough: {
|
|
44
|
+
type: Array as PropType<string[]>,
|
|
45
|
+
default: () => [],
|
|
46
|
+
},
|
|
47
|
+
theme: {
|
|
48
|
+
type: String as PropType<string>,
|
|
49
|
+
default: 'primary',
|
|
50
|
+
validator(value: string) {
|
|
51
|
+
return propValidators.theme.includes(value);
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
round: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: true,
|
|
57
|
+
},
|
|
58
|
+
size: {
|
|
59
|
+
type: String as PropType<string>,
|
|
60
|
+
default: 'normal',
|
|
61
|
+
validator(value: string) {
|
|
62
|
+
return propValidators.size.includes(value);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const slots = useSlots();
|
|
68
|
+
const hasDescriptionSlot = computed(() => slots.description !== undefined);
|
|
69
|
+
const hasIconOnSlot = computed(() => slots.iconOn !== undefined);
|
|
70
|
+
const hasIconOffSlot = computed(() => slots.iconOff !== undefined);
|
|
71
|
+
|
|
72
|
+
const formTheme = computed(() => {
|
|
73
|
+
return fieldHasError ? 'error' : theme;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const modelValue = defineModel();
|
|
77
|
+
const { elementClasses, updateElementClasses } = useStyleClassPassthrough(styleClassPassthrough);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style lang="css">
|
|
81
|
+
.toggle-switch-with-label {
|
|
82
|
+
--_transition-duration: 0.4s;
|
|
83
|
+
|
|
84
|
+
.toggle-switch-label {
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srcdev-nuxt-forms",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.30",
|
|
5
5
|
"main": "nuxt.config.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"clean": "rm -rf .nuxt && rm -rf .output && rm -rf .playground/.nuxt && rm -rf .playground/.output",
|
|
8
|
+
"cleanall": "rm -rf node_modules && rm -rf .nuxt && rm -rf .output && rm -rf .playground/.nuxt && rm -rf .playground/.output && rm -rf .playground/node_modules && rm package-lock.json",
|
|
8
9
|
"reinstall": "rm -rf node_modules && npm install",
|
|
9
10
|
"dev": "nuxi dev .playground",
|
|
10
11
|
"build": "nuxt build .playground",
|
|
@@ -23,12 +24,11 @@
|
|
|
23
24
|
"types/"
|
|
24
25
|
],
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"@nuxt/
|
|
28
|
-
"@nuxt/icon": "1.10.1",
|
|
27
|
+
"@nuxt/eslint-config": "0.7.3",
|
|
28
|
+
"@nuxt/icon": "1.10.2",
|
|
29
29
|
"@nuxt/test-utils": "3.15.1",
|
|
30
30
|
"@vue/test-utils": "2.4.6",
|
|
31
|
-
"eslint": "9.
|
|
31
|
+
"eslint": "9.17.0",
|
|
32
32
|
"happy-dom": "15.11.7",
|
|
33
33
|
"nuxt": "3.14.1592",
|
|
34
34
|
"release-it": "17.10.0",
|
|
@@ -37,15 +37,18 @@
|
|
|
37
37
|
"vue": "3.5.13"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@iconify-json/carbon": "1.2.
|
|
41
|
-
"@iconify-json/gridicons": "1.2.
|
|
42
|
-
"@iconify-json/
|
|
43
|
-
"@iconify-json/
|
|
40
|
+
"@iconify-json/carbon": "1.2.5",
|
|
41
|
+
"@iconify-json/gridicons": "1.2.2",
|
|
42
|
+
"@iconify-json/material-symbols": "1.2.12",
|
|
43
|
+
"@iconify-json/material-symbols-light": "1.2.12",
|
|
44
|
+
"@iconify-json/ph": "1.2.2",
|
|
45
|
+
"@iconify-json/radix-icons": "1.2.2",
|
|
46
|
+
"@iconify-json/ri": "1.2.5",
|
|
44
47
|
"@nuxtjs/storybook": "8.3.2",
|
|
45
|
-
"@storybook/addon-essentials": "8.4.
|
|
46
|
-
"@storybook/addon-interactions": "8.4.
|
|
47
|
-
"@storybook/addon-links": "8.4.
|
|
48
|
-
"@storybook/vue3": "8.4.
|
|
48
|
+
"@storybook/addon-essentials": "8.4.7",
|
|
49
|
+
"@storybook/addon-interactions": "8.4.7",
|
|
50
|
+
"@storybook/addon-links": "8.4.7",
|
|
51
|
+
"@storybook/vue3": "8.4.7",
|
|
49
52
|
"http-proxy-middleware": "3.0.3",
|
|
50
53
|
"modern-normalize": "3.0.1",
|
|
51
54
|
"zod": "3.24.1"
|