@fiscozen/checkbox 0.1.0
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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/checkbox.js +301 -0
- package/dist/checkbox.umd.cjs +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/src/FzCheckbox.vue.d.ts +95 -0
- package/dist/src/FzCheckboxGroup.vue.d.ts +70 -0
- package/dist/src/__test__/FzCheckbox.test.d.ts +1 -0
- package/dist/src/__test__/FzCheckboxGroup.test.d.ts +1 -0
- package/dist/src/common.d.ts +4 -0
- package/dist/src/components/FzCheckboxErrorText.vue.d.ts +24 -0
- package/dist/src/components/FzCheckboxGroupOption.vue.d.ts +82 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/types.d.ts +76 -0
- package/dist/style.css +1 -0
- package/package.json +49 -0
- package/src/FzCheckbox.vue +139 -0
- package/src/FzCheckboxGroup.vue +74 -0
- package/src/__test__/FzCheckbox.test.ts +88 -0
- package/src/__test__/FzCheckboxGroup.test.ts +100 -0
- package/src/__test__/__snapshots__/FzCheckbox.test.ts.snap +21 -0
- package/src/common.ts +4 -0
- package/src/components/FzCheckboxErrorText.vue +20 -0
- package/src/components/FzCheckboxGroupOption.vue +96 -0
- package/src/index.ts +3 -0
- package/src/types.ts +75 -0
- package/tsconfig.json +4 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type FzCheckboxProps = {
|
|
2
|
+
/**
|
|
3
|
+
* The label of the checkbox
|
|
4
|
+
*/
|
|
5
|
+
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* The value of the checkbox. If not provided, the value will be the label
|
|
8
|
+
*/
|
|
9
|
+
value?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The size of the checkbox
|
|
12
|
+
*/
|
|
13
|
+
size: "sm" | "md";
|
|
14
|
+
/**
|
|
15
|
+
* If the checkbox is checked
|
|
16
|
+
*/
|
|
17
|
+
checked?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* if the checkbox is indeterminate
|
|
20
|
+
*/
|
|
21
|
+
indeterminate?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* If true, the checkbox will be emphasized
|
|
24
|
+
*/
|
|
25
|
+
emphasis?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* If true, the checkbox will be disabled
|
|
28
|
+
*/
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* If true, the checkbox will be in an error state
|
|
32
|
+
*/
|
|
33
|
+
error?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* If the checkbox is required
|
|
36
|
+
*/
|
|
37
|
+
required?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* If the checkbox is standalone
|
|
40
|
+
*/
|
|
41
|
+
standalone?: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type FzCheckboxGroupProps = {
|
|
44
|
+
/**
|
|
45
|
+
* The label of the checkbox group
|
|
46
|
+
*/
|
|
47
|
+
label: string;
|
|
48
|
+
/**
|
|
49
|
+
* The size of the checkbox
|
|
50
|
+
*/
|
|
51
|
+
size: "sm" | "md";
|
|
52
|
+
/**
|
|
53
|
+
* The checkbox to render
|
|
54
|
+
*/
|
|
55
|
+
options: ParentCheckbox[];
|
|
56
|
+
/**
|
|
57
|
+
* If true, the checkbox will be emphasized
|
|
58
|
+
*/
|
|
59
|
+
emphasis?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* If true, the checkbox will be disabled
|
|
62
|
+
*/
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* If true, the checkbox will be in an error state
|
|
66
|
+
*/
|
|
67
|
+
error?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* If the checkbox group is required
|
|
70
|
+
*/
|
|
71
|
+
required?: boolean;
|
|
72
|
+
};
|
|
73
|
+
export type ParentCheckbox = ChildCheckbox & {
|
|
74
|
+
children?: ChildCheckbox[];
|
|
75
|
+
};
|
|
76
|
+
export type ChildCheckbox = Omit<FzCheckboxProps, "size">;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
input[type=checkbox][data-v-7bafffef]{width:0;height:0}input[type=checkbox]+label[data-v-7bafffef]{cursor:pointer}input[type=checkbox]+label[data-v-7bafffef]:hover{cursor:pointer}input[type=checkbox]:focus+label div[data-v-7bafffef]:after{border-radius:3px;content:"";border-style:solid;border-width:1px;@apply border-blue-500;top:0;left:0;right:0;bottom:0;position:absolute}.checkbox--small:focus+label div[data-v-7bafffef]:after{transform:translate(-.2px,.4px)}.checkbox-group--child-container__small[data-v-c563830e]{margin-top:5px;padding-left:24px}.checkbox-group--child-container__medium[data-v-c563830e]{margin-top:6px;padding-left:24px}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fiscozen/checkbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design System Checkbox and Checkbox Group component",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "Cristian Barraco",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@fiscozen/badge": "^0.1.0",
|
|
11
|
+
"@fiscozen/composables": "^0.1.8"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"tailwindcss": "^3.4.1",
|
|
15
|
+
"vue": "^3.4.13",
|
|
16
|
+
"@fiscozen/icons": "^0.1.3"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@rushstack/eslint-patch": "^1.3.3",
|
|
20
|
+
"@types/jsdom": "^21.1.6",
|
|
21
|
+
"@types/node": "^18.19.3",
|
|
22
|
+
"@vitejs/plugin-vue": "^4.5.2",
|
|
23
|
+
"@vitest/coverage-v8": "^1.2.1",
|
|
24
|
+
"@vue/test-utils": "^2.4.3",
|
|
25
|
+
"@vue/tsconfig": "^0.5.0",
|
|
26
|
+
"vite-plugin-dts": "^3.8.3",
|
|
27
|
+
"eslint": "^8.49.0",
|
|
28
|
+
"jsdom": "^23.0.1",
|
|
29
|
+
"prettier": "^3.0.3",
|
|
30
|
+
"typescript": "~5.3.0",
|
|
31
|
+
"vite": "^5.0.10",
|
|
32
|
+
"vitest": "^1.2.0",
|
|
33
|
+
"vue-tsc": "^1.8.25",
|
|
34
|
+
"@awesome.me/kit-8137893ad3": "^1.0.65",
|
|
35
|
+
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
|
36
|
+
"@fortawesome/vue-fontawesome": "^3.0.6",
|
|
37
|
+
"@fiscozen/eslint-config": "^0.1.0",
|
|
38
|
+
"@fiscozen/prettier-config": "^0.1.0",
|
|
39
|
+
"@fiscozen/tsconfig": "^0.1.0",
|
|
40
|
+
"@fiscozen/icons": "^0.1.3"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"coverage": "vitest run --coverage",
|
|
45
|
+
"format": "prettier --write src/",
|
|
46
|
+
"test:unit": "vitest run",
|
|
47
|
+
"build": "vue-tsc && vite build"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex justify-center flex-col w-fit">
|
|
3
|
+
<input
|
|
4
|
+
type="checkbox"
|
|
5
|
+
:id="value.replace(/ /g, '-').toLowerCase()"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:checked="checked"
|
|
8
|
+
:class="staticInputClass"
|
|
9
|
+
:required="required"
|
|
10
|
+
:value="value"
|
|
11
|
+
@change="emit('change', $event)"
|
|
12
|
+
v-model="model"
|
|
13
|
+
:indeterminate="indeterminate"
|
|
14
|
+
ref="refCheckbox"
|
|
15
|
+
/>
|
|
16
|
+
<label
|
|
17
|
+
:for="value.replace(/ /g, '-').toLowerCase()"
|
|
18
|
+
:class="[staticLabelClass, computedLabelClass]"
|
|
19
|
+
>
|
|
20
|
+
<FzIcon
|
|
21
|
+
:name="computedName"
|
|
22
|
+
:size="size"
|
|
23
|
+
:class="[staticIconClass, computedIconClasses]"
|
|
24
|
+
:variant="computedVariant"
|
|
25
|
+
/>
|
|
26
|
+
<span class="w-fit">{{ standalone ? "" : label }}</span>
|
|
27
|
+
</label>
|
|
28
|
+
<FzRadioErrorText :size="size" v-if="error && $slots.error">
|
|
29
|
+
<slot name="error" />
|
|
30
|
+
</FzRadioErrorText>
|
|
31
|
+
<slot name="children" />
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { computed, onMounted, ref } from "vue";
|
|
37
|
+
import { FzCheckboxProps } from "./types";
|
|
38
|
+
import { mapSizeToClasses } from "./common";
|
|
39
|
+
import FzRadioErrorText from "./components/FzCheckboxErrorText.vue";
|
|
40
|
+
import { FzIcon } from "@fiscozen/icons";
|
|
41
|
+
|
|
42
|
+
const props = withDefaults(defineProps<FzCheckboxProps>(), {
|
|
43
|
+
size: "md",
|
|
44
|
+
indeterminate: false,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const value = props.value || props.label;
|
|
48
|
+
|
|
49
|
+
const model = defineModel<boolean | string[]>({
|
|
50
|
+
required: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const emit = defineEmits(["change"]);
|
|
54
|
+
const refCheckbox = ref<HTMLInputElement | null>(null);
|
|
55
|
+
|
|
56
|
+
const staticInputClass = "w-0 h-0 peer";
|
|
57
|
+
const staticLabelClass = `
|
|
58
|
+
flex items-start gap-4 hover:cursor-pointer
|
|
59
|
+
peer-focus:[&_div]:after:border-1
|
|
60
|
+
peer-focus:[&_div]:after:border-solid
|
|
61
|
+
peer-focus:[&_div]:after:rounded-[3px]
|
|
62
|
+
peer-focus:[&_div]:after:border-blue-500
|
|
63
|
+
peer-focus:[&_div]:after:content-['']
|
|
64
|
+
peer-focus:[&_div]:after:top-0
|
|
65
|
+
peer-focus:[&_div]:after:left-0
|
|
66
|
+
peer-focus:[&_div]:after:right-0
|
|
67
|
+
peer-focus:[&_div]:after:bottom-0
|
|
68
|
+
peer-focus:[&_div]:after:absolute
|
|
69
|
+
`;
|
|
70
|
+
const staticIconClass = "relative";
|
|
71
|
+
|
|
72
|
+
const computedLabelClass = computed(() => [
|
|
73
|
+
mapSizeToClasses[props.size],
|
|
74
|
+
getTextClassForLabel(),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const computedIconClasses = computed(() => [
|
|
78
|
+
props.size === "sm" ? "mt-1" : "",
|
|
79
|
+
props.size === "md" ? "mt-2" : "",
|
|
80
|
+
getTextClassForIcon(),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const computedName = computed(() => {
|
|
84
|
+
if (props.indeterminate) {
|
|
85
|
+
return "square-minus";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return checkValueIsInModel() ? "square-check" : "square";
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const computedVariant = computed(() => {
|
|
92
|
+
return !props.indeterminate && !checkValueIsInModel() ? "far" : "fas";
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const checkValueIsInModel = () => {
|
|
96
|
+
if (typeof model.value === "boolean") {
|
|
97
|
+
return model.value;
|
|
98
|
+
} else {
|
|
99
|
+
return model.value.includes(value);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getTextClassForLabel = () => {
|
|
104
|
+
switch (true) {
|
|
105
|
+
case props.disabled:
|
|
106
|
+
return "text-grey-300";
|
|
107
|
+
case props.error:
|
|
108
|
+
return "text-semantic-error";
|
|
109
|
+
case props.emphasis:
|
|
110
|
+
return "text-grey-500 peer-checked:[&_div]:text-blue-500 peer-indeterminate:[&_div]:text-blue-500";
|
|
111
|
+
default:
|
|
112
|
+
return "text-grey-500";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getTextClassForIcon = () => {
|
|
117
|
+
switch (true) {
|
|
118
|
+
case props.disabled:
|
|
119
|
+
return "text-grey-300";
|
|
120
|
+
case props.error:
|
|
121
|
+
return "text-semantic-error";
|
|
122
|
+
case props.emphasis:
|
|
123
|
+
default:
|
|
124
|
+
return "text-grey-500";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
onMounted(() => {
|
|
129
|
+
if (typeof model.value === "boolean") {
|
|
130
|
+
if (model.value) refCheckbox.value?.dispatchEvent(new Event("change"));
|
|
131
|
+
else if (props.checked !== undefined) model.value = props.checked;
|
|
132
|
+
} else {
|
|
133
|
+
if (model.value.includes(value))
|
|
134
|
+
refCheckbox.value?.dispatchEvent(new Event("change"));
|
|
135
|
+
else if (props.checked) model.value.push(value);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
<style scoped></style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="[staticContainerClass, computedContainerClass]" :id="id">
|
|
3
|
+
<label :for="id" :class="[staticLabeldClass, computedLabelClass]">
|
|
4
|
+
<span>{{ label }}<span v-if="required"> *</span></span>
|
|
5
|
+
<p :class="computedHelpTextClass" v-if="$slots.help">
|
|
6
|
+
<slot name="help" />
|
|
7
|
+
</p>
|
|
8
|
+
</label>
|
|
9
|
+
<div :class="[staticSlotContainerClass, computedSlotContainerClass]">
|
|
10
|
+
<FzCheckboxGroupOption
|
|
11
|
+
v-for="option in options"
|
|
12
|
+
:key="option.value"
|
|
13
|
+
v-model="model"
|
|
14
|
+
v-bind="option"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
:emphasis="emphasis"
|
|
17
|
+
:error="error"
|
|
18
|
+
:size="size"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
<FzCheckboxErrorText :size="size" v-if="error && $slots.error">
|
|
22
|
+
<slot name="error" />
|
|
23
|
+
</FzCheckboxErrorText>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { computed } from "vue";
|
|
29
|
+
import { FzCheckboxGroupProps, ParentCheckbox } from "./types";
|
|
30
|
+
import FzCheckboxErrorText from "./components/FzCheckboxErrorText.vue";
|
|
31
|
+
import { mapSizeToClasses } from "./common";
|
|
32
|
+
import FzCheckboxGroupOption from "./components/FzCheckboxGroupOption.vue";
|
|
33
|
+
|
|
34
|
+
const props = defineProps<FzCheckboxGroupProps>();
|
|
35
|
+
const id = `fz-checkbox-group-${generateRandomId()}`;
|
|
36
|
+
|
|
37
|
+
const computedHelpTextClass = computed(() => [
|
|
38
|
+
props.size === "sm" ? "text-xs" : "text-sm",
|
|
39
|
+
props.disabled ? "text-grey-400" : "text-grey-500",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const model = defineModel<string[]>({
|
|
43
|
+
required: true,
|
|
44
|
+
default: [],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const staticLabeldClass = "flex flex-col";
|
|
48
|
+
const staticContainerClass = "flex flex-col";
|
|
49
|
+
const staticSlotContainerClass = "flex flex-col";
|
|
50
|
+
|
|
51
|
+
const computedLabelClass = computed(() => [
|
|
52
|
+
mapSizeToClasses[props.size],
|
|
53
|
+
props.size === "sm" ? "gap-4" : "",
|
|
54
|
+
props.size === "md" ? "gap-6" : "",
|
|
55
|
+
props.disabled ? "text-grey-400" : "",
|
|
56
|
+
!props.disabled ? "text-grey-500" : "",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const computedContainerClass = computed(() => [
|
|
60
|
+
mapSizeToClasses[props.size],
|
|
61
|
+
props.size === "sm" ? "gap-10" : "",
|
|
62
|
+
props.size === "md" ? "gap-12" : "",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const computedSlotContainerClass = computed(() => [
|
|
66
|
+
mapSizeToClasses[props.size],
|
|
67
|
+
props.size === "sm" ? "gap-6" : "",
|
|
68
|
+
props.size === "md" ? "gap-8" : "",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function generateRandomId() {
|
|
72
|
+
return Math.random().toString(36).slice(2, 9);
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// FzCheckbox.spec.ts
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import FzCheckbox from "../FzCheckbox.vue";
|
|
5
|
+
|
|
6
|
+
describe("FzCheckbox", () => {
|
|
7
|
+
it("renders correctly", async () => {
|
|
8
|
+
const wrapper = mount(FzCheckbox, {
|
|
9
|
+
props: {
|
|
10
|
+
label: "Test Checkbox",
|
|
11
|
+
value: "test",
|
|
12
|
+
size: "md",
|
|
13
|
+
modelValue: false,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
await wrapper.vm.$nextTick();
|
|
18
|
+
expect(wrapper.html()).toContain("Test Checkbox");
|
|
19
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("emits an update event when clicked", async () => {
|
|
23
|
+
const wrapper = mount(FzCheckbox, {
|
|
24
|
+
props: {
|
|
25
|
+
label: "Test Checkbox",
|
|
26
|
+
value: "test",
|
|
27
|
+
modelValue: false,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
await wrapper.find("input").trigger("click");
|
|
31
|
+
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is checked when v-model is true", async () => {
|
|
35
|
+
const wrapper = mount(FzCheckbox, {
|
|
36
|
+
props: {
|
|
37
|
+
label: "Test Checkbox",
|
|
38
|
+
value: "test",
|
|
39
|
+
modelValue: true,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
await wrapper.vm.$nextTick();
|
|
43
|
+
expect(wrapper.find("input").element.checked).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is unchecked when v-model is false", async () => {
|
|
47
|
+
const wrapper = mount(FzCheckbox, {
|
|
48
|
+
props: {
|
|
49
|
+
label: "Test Checkbox",
|
|
50
|
+
value: "test",
|
|
51
|
+
modelValue: false,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await wrapper.vm.$nextTick();
|
|
56
|
+
expect(wrapper.find("input").element.checked).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should be emphasized", async () => {
|
|
60
|
+
const wrapper = mount(FzCheckbox, {
|
|
61
|
+
props: {
|
|
62
|
+
label: "Test Checkbox",
|
|
63
|
+
value: "test",
|
|
64
|
+
size: "md",
|
|
65
|
+
modelValue: false,
|
|
66
|
+
emphasis: true,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await wrapper.vm.$nextTick();
|
|
70
|
+
expect(wrapper.find("label").classes()).toContain(
|
|
71
|
+
"peer-checked:[&_div]:text-blue-500",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should be disabled", async () => {
|
|
76
|
+
const wrapper = mount(FzCheckbox, {
|
|
77
|
+
props: {
|
|
78
|
+
label: "Test Checkbox",
|
|
79
|
+
value: "test",
|
|
80
|
+
size: "md",
|
|
81
|
+
modelValue: false,
|
|
82
|
+
disabled: true,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
await wrapper.vm.$nextTick();
|
|
86
|
+
expect(wrapper.find("input").element.disabled).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import FzCheckboxGroup from "../FzCheckboxGroup.vue";
|
|
4
|
+
import FzCheckbox from "../FzCheckbox.vue";
|
|
5
|
+
|
|
6
|
+
describe("FzCheckboxGroup", () => {
|
|
7
|
+
it("renders correctly", async () => {
|
|
8
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
9
|
+
props: {
|
|
10
|
+
label: "Test Checkbox Group",
|
|
11
|
+
size: "md",
|
|
12
|
+
modelValue: [],
|
|
13
|
+
options: [
|
|
14
|
+
{ label: "Option 1", value: "option1" },
|
|
15
|
+
{ label: "Option 2", value: "option2" },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await wrapper.vm.$nextTick();
|
|
21
|
+
expect(wrapper.html()).toContain("Test Checkbox Group");
|
|
22
|
+
expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("displays error text when error prop is true", async () => {
|
|
26
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
27
|
+
props: {
|
|
28
|
+
label: "Test Checkbox Group",
|
|
29
|
+
size: "md",
|
|
30
|
+
modelValue: [],
|
|
31
|
+
options: [
|
|
32
|
+
{ label: "Option 1", value: "option1" },
|
|
33
|
+
{ label: "Option 2", value: "option2" },
|
|
34
|
+
],
|
|
35
|
+
error: true,
|
|
36
|
+
},
|
|
37
|
+
slots: {
|
|
38
|
+
error: "Test error message",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await wrapper.vm.$nextTick();
|
|
43
|
+
expect(wrapper.html()).toContain("Test error message");
|
|
44
|
+
expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
|
|
45
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("error")).toBe(
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("error")).toBe(
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("has disabled checkboxes when disabled prop is true", async () => {
|
|
54
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
55
|
+
props: {
|
|
56
|
+
label: "Test Checkbox Group",
|
|
57
|
+
size: "md",
|
|
58
|
+
modelValue: [],
|
|
59
|
+
options: [
|
|
60
|
+
{ label: "Option 1", value: "option1" },
|
|
61
|
+
{ label: "Option 2", value: "option2" },
|
|
62
|
+
],
|
|
63
|
+
disabled: true,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await wrapper.vm.$nextTick();
|
|
68
|
+
expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
|
|
69
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("disabled")).toBe(
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("disabled")).toBe(
|
|
73
|
+
true,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("display emphasized checkboxes when emphasis prop is true", async () => {
|
|
78
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
79
|
+
props: {
|
|
80
|
+
label: "Test Checkbox Group",
|
|
81
|
+
size: "md",
|
|
82
|
+
modelValue: [],
|
|
83
|
+
options: [
|
|
84
|
+
{ label: "Option 1", value: "option1" },
|
|
85
|
+
{ label: "Option 2", value: "option2" },
|
|
86
|
+
],
|
|
87
|
+
emphasis: true,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await wrapper.vm.$nextTick();
|
|
92
|
+
expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
|
|
93
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("emphasis")).toBe(
|
|
94
|
+
true,
|
|
95
|
+
);
|
|
96
|
+
expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("emphasis")).toBe(
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`FzCheckbox > renders correctly 1`] = `
|
|
4
|
+
"<div class="flex justify-center flex-col w-fit"><input type="checkbox" id="test" class="w-0 h-0 peer" value="test"><label for="test" class="flex items-start gap-4 hover:cursor-pointer
|
|
5
|
+
peer-focus:[&_div]:after:border-1
|
|
6
|
+
peer-focus:[&_div]:after:border-solid
|
|
7
|
+
peer-focus:[&_div]:after:rounded-[3px]
|
|
8
|
+
peer-focus:[&_div]:after:border-blue-500
|
|
9
|
+
peer-focus:[&_div]:after:content-['']
|
|
10
|
+
peer-focus:[&_div]:after:top-0
|
|
11
|
+
peer-focus:[&_div]:after:left-0
|
|
12
|
+
peer-focus:[&_div]:after:right-0
|
|
13
|
+
peer-focus:[&_div]:after:bottom-0
|
|
14
|
+
peer-focus:[&_div]:after:absolute text-md text-grey-500">
|
|
15
|
+
<div class="flex items-center justify-center w-[20px] h-[20px] relative mt-2 text-grey-500"><svg class="svg-inline--fa fa-square h-[16px]" aria-hidden="true" focusable="false" data-prefix="far" data-icon="square" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
16
|
+
<path class="" fill="currentColor" d="M384 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H384zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"></path>
|
|
17
|
+
</svg></div><span class="w-fit">Test Checkbox</span>
|
|
18
|
+
</label>
|
|
19
|
+
<!--v-if-->
|
|
20
|
+
</div>"
|
|
21
|
+
`;
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<p :class="[staticErrorTextClass, computedErrorTextClass]">
|
|
3
|
+
<FzIcon
|
|
4
|
+
class="text-semantic-error"
|
|
5
|
+
name="triangle-exclamation"
|
|
6
|
+
:size="size"
|
|
7
|
+
/>
|
|
8
|
+
<slot />
|
|
9
|
+
</p>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed } from "vue";
|
|
14
|
+
import { mapSizeToClasses } from "../common";
|
|
15
|
+
import { FzIcon } from "@fiscozen/icons";
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{ errorText?: string; size: "sm" | "md" }>();
|
|
18
|
+
const staticErrorTextClass = "flex w-full gap-4 items-center";
|
|
19
|
+
const computedErrorTextClass = computed(() => [mapSizeToClasses[props.size]]);
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FzCheckbox
|
|
3
|
+
v-model="model"
|
|
4
|
+
:value="props.value"
|
|
5
|
+
:label="props.label"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:emphasis="emphasis"
|
|
8
|
+
:error="error"
|
|
9
|
+
:size="size"
|
|
10
|
+
:indeterminate="isIndeterminate"
|
|
11
|
+
@change="onCheckboxParentChange"
|
|
12
|
+
>
|
|
13
|
+
<template #children v-if="children?.length">
|
|
14
|
+
<div :class="[staticChildContainerClass, computedChildContainerClasses]">
|
|
15
|
+
<FzCheckbox
|
|
16
|
+
v-for="child in children"
|
|
17
|
+
:key="child.value"
|
|
18
|
+
v-model="model"
|
|
19
|
+
v-bind="child"
|
|
20
|
+
:disabled="disabled"
|
|
21
|
+
:emphasis="emphasis"
|
|
22
|
+
:error="error"
|
|
23
|
+
:size="size"
|
|
24
|
+
@change="handleCheckboxParentChange"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
</FzCheckbox>
|
|
29
|
+
</template>
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed } from "vue";
|
|
32
|
+
import FzCheckbox from "../FzCheckbox.vue";
|
|
33
|
+
import { ParentCheckbox } from "../types";
|
|
34
|
+
|
|
35
|
+
const props = defineProps<ParentCheckbox & { size: "sm" | "md" }>();
|
|
36
|
+
|
|
37
|
+
const model = defineModel<string[]>({
|
|
38
|
+
required: true,
|
|
39
|
+
default: [],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const staticChildContainerClass = "flex flex-col justify-center pl-24";
|
|
43
|
+
|
|
44
|
+
const computedChildContainerClasses = computed(() => [
|
|
45
|
+
props.size === "sm" ? "gap-6 mt-6" : "",
|
|
46
|
+
props.size === "md" ? "gap-8 mt-8" : "",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const isIndeterminate = computed(() => {
|
|
50
|
+
if (!props.children) return false;
|
|
51
|
+
|
|
52
|
+
const numChecked = props.children.filter((child) =>
|
|
53
|
+
model.value.includes(child.value || child.label),
|
|
54
|
+
).length;
|
|
55
|
+
return numChecked > 0 && numChecked < props.children.length;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function handleCheckboxParentChange() {
|
|
59
|
+
if (!props.children) return;
|
|
60
|
+
const numChecked = props.children.filter((child) =>
|
|
61
|
+
model.value.includes(child.value || child.label),
|
|
62
|
+
).length;
|
|
63
|
+
|
|
64
|
+
if (numChecked === props.children.length) {
|
|
65
|
+
// push parent value to model (using concat to force reactivity, push seems to not work)
|
|
66
|
+
model.value = model.value.concat(props.value || props.label);
|
|
67
|
+
} else {
|
|
68
|
+
// remove parent value from model if it exists
|
|
69
|
+
if (model.value.includes(props.value || props.label))
|
|
70
|
+
model.value = model.value.filter(
|
|
71
|
+
(value) => value !== (props.value || props.label),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onCheckboxParentChange() {
|
|
77
|
+
if (!props.children) return;
|
|
78
|
+
if (model.value.includes(props.value || props.label)) {
|
|
79
|
+
// push all children values to model that are not already in model
|
|
80
|
+
model.value = model.value.concat(
|
|
81
|
+
props.children
|
|
82
|
+
?.map((child) => child.value || child.label)
|
|
83
|
+
.filter((value) => !model.value.includes(value)),
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
// remove all children values from model
|
|
87
|
+
model.value = model.value.filter(
|
|
88
|
+
(value) =>
|
|
89
|
+
!props.children
|
|
90
|
+
?.map((child) => child.value || child.label)
|
|
91
|
+
.includes(value),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
<style scoped></style>
|
package/src/index.ts
ADDED