feat: add radio and radio-group component

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/581/head
Ryan Wang 2022-04-19 17:56:21 +08:00
parent c4479ff1fe
commit d66ee9c391
8 changed files with 411 additions and 7 deletions

View File

@ -0,0 +1,69 @@
<script lang="ts" setup>
import { computed } from "vue";
const props = defineProps({
modelValue: {
type: [String, Number, Boolean],
},
value: {
type: [String, Number, Boolean],
},
label: {
type: String,
},
name: {
type: String,
},
});
const emit = defineEmits(["update:modelValue", "change"]);
const id = ["radio", props.name, props.value]
.filter((item) => !!item)
.join("-");
const checked = computed(() => props.modelValue === props.value);
function handleChange(e: Event) {
const { value } = e.target as HTMLInputElement;
emit("update:modelValue", value);
emit("change", value);
}
</script>
<template>
<div class="radio-wrapper" :class="{ 'radio-wrapper-checked': checked }">
<div class="radio-inner">
<input
type="radio"
:id="id"
:value="value"
:checked="checked"
:name="name"
@change="handleChange"
/>
</div>
<label class="radio-label" :for="id" v-if="label">
{{ label }}
</label>
</div>
</template>
<style lang="scss">
.radio-wrapper {
@apply flex;
@apply items-center;
@apply box-border;
@apply flex-grow-0;
.radio-inner {
@apply self-center;
@apply relative;
}
.radio-label {
@apply flex;
@apply self-center;
@apply items-start;
@apply ml-3;
}
}
</style>

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { VRadio } from "./index";
import type { PropType } from "vue";
defineProps({
modelValue: {
type: [String, Number, Boolean],
},
options: {
type: Object as PropType<Array<Record<string, string | number | boolean>>>,
},
valueKey: {
type: String,
default: "value",
},
labelKey: {
type: String,
default: "label",
},
name: {
type: String,
},
});
const emit = defineEmits(["update:modelValue", "change"]);
function handleChange(value: string | number | boolean) {
emit("update:modelValue", value);
emit("change", value);
}
</script>
<template>
<div class="radio-group-wrapper">
<VRadio
v-for="(option, index) in options"
:key="index"
:label="option[labelKey] + ''"
:model-value="modelValue"
:value="option[valueKey]"
:name="name"
@change="handleChange"
/>
</div>
</template>
<style lang="scss"></style>

View File

@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";
import { VRadio } from "../index";
import { mount } from "@vue/test-utils";
describe("Radio", () => {
it("should render", () => {
expect(VRadio).toBeDefined();
expect(mount(VRadio).html()).toMatchSnapshot();
});
it("should work with v-model", async function () {
const wrapper = mount({
data() {
return {
value: "",
};
},
template: "<v-radio v-model='value' name='test' value='bar' />",
components: {
VRadio,
},
});
expect(wrapper.findComponent(VRadio).classes()).not.toContain(
"radio-wrapper-checked"
);
await wrapper.setData({ value: "bar" });
expect(wrapper.findComponent(VRadio).classes()).toContain(
"radio-wrapper-checked"
);
});
it("should work with label prop", async function () {
const wrapper = mount(VRadio, {
props: {
value: "foo",
},
});
expect(wrapper.html()).not.toContain("label");
await wrapper.setProps({ label: "foo" });
expect(wrapper.html()).toContain("label");
expect(wrapper.find("label").text()).toBe("foo");
});
it("should work with multiple radio", async function () {
const wrapper = mount({
data() {
return {
value: "foo",
};
},
template:
"<v-radio v-model='value' name='test' value='foo' label='foo' />" +
"<v-radio v-model='value' name='test' value='bar' label='bar' />",
components: {
VRadio,
},
});
expect(wrapper.findAllComponents(VRadio).length).toBe(2);
expect(wrapper.findAllComponents(VRadio)[0].classes()).toContain(
"radio-wrapper-checked"
);
// set value to bar
await wrapper.setData({ value: "bar" });
expect(wrapper.findAllComponents(VRadio)[0].classes()).not.toContain(
"radio-wrapper-checked"
);
expect(wrapper.findAllComponents(VRadio)[1].classes()).toContain(
"radio-wrapper-checked"
);
// click on the first radio
await wrapper
.findAllComponents(VRadio)[0]
.find('input[type="radio"]')
.trigger("change");
expect(wrapper.findAllComponents(VRadio)[0].classes()).toContain(
"radio-wrapper-checked"
);
// click on the second radio
await wrapper
.findAllComponents(VRadio)[1]
.find('input[type="radio"]')
.trigger("change");
expect(wrapper.findAllComponents(VRadio)[1].classes()).toContain(
"radio-wrapper-checked"
);
});
});

View File

@ -0,0 +1,135 @@
import { describe, expect, it } from "vitest";
import { VRadio, VRadioGroup } from "../index";
import { mount } from "@vue/test-utils";
describe("RadioGroup", () => {
it("should render", function () {
expect(VRadioGroup).toBeDefined();
expect(
mount(VRadioGroup, {
props: {
options: [
{
value: "foo",
label: "foo",
},
{
value: "bar",
label: "bar",
},
],
},
}).html()
).toMatchSnapshot();
});
it("should work with options prop", function () {
const wrapper = mount({
data() {
return {
options: [
{
value: "foo",
label: "foo",
},
{
value: "bar",
label: "bar",
},
],
};
},
template: '<v-radio-group :options="options" />',
components: {
VRadioGroup,
},
});
expect(wrapper.findAllComponents(VRadio).length).toBe(2);
expect(wrapper.findAllComponents(VRadio)[0].vm.$props.value).toBe("foo");
expect(wrapper.findAllComponents(VRadio)[0].vm.$props.label).toBe("foo");
expect(wrapper.findAllComponents(VRadio)[1].vm.$props.value).toBe("bar");
expect(wrapper.findAllComponents(VRadio)[1].vm.$props.label).toBe("bar");
});
it("should work with v-model", async function () {
const wrapper = mount({
data() {
return {
value: "foo",
options: [
{
value: "foo",
label: "foo",
},
{
value: "bar",
label: "bar",
},
],
};
},
template: '<v-radio-group v-model="value" :options="options" />',
components: {
VRadioGroup,
},
});
expect(wrapper.findAllComponents(VRadio)[0].classes()).toContain(
"radio-wrapper-checked"
);
await wrapper
.findAllComponents(VRadio)[1]
.find('input[type="radio"]')
.trigger("change");
expect(wrapper.findAllComponents(VRadio)[0].classes()).not.toContain(
"radio-wrapper-checked"
);
expect(wrapper.findAllComponents(VRadio)[1].classes()).toContain(
"radio-wrapper-checked"
);
});
it("should work with valueKey and labelKey props", async function () {
const wrapper = mount({
data() {
return {
value: "foo",
options: [
{
id: "foo",
name: "foo",
},
{
id: "bar",
name: "bar",
},
],
};
},
template:
'<v-radio-group v-model="value" :options="options" value-key="id" label-key="name" />',
components: {
VRadioGroup,
},
});
expect(
wrapper.findAllComponents(VRadio)[0].find("input").attributes("value")
).toBe("foo");
expect(
wrapper.findAllComponents(VRadio)[0].find(".radio-label").text()
).toBe("foo");
await wrapper
.findAllComponents(VRadio)[1]
.find('input[type="radio"]')
.trigger("change");
expect(wrapper.findAllComponents(VRadio)[1].classes()).toContain(
"radio-wrapper-checked"
);
});
});

View File

@ -0,0 +1,8 @@
// Vitest Snapshot v1
exports[`Radio > should render 1`] = `
"<div class=\\"radio-wrapper radio-wrapper-checked\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio\\" value=\\"false\\"></div>
<!--v-if-->
</div>"
`;

View File

@ -0,0 +1,12 @@
// Vitest Snapshot v1
exports[`RadioGroup > should render 1`] = `
"<div class=\\"radio-group-wrapper\\">
<div class=\\"radio-wrapper\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio-foo\\" value=\\"foo\\"></div><label class=\\"radio-label\\" for=\\"radio-foo\\">foo</label>
</div>
<div class=\\"radio-wrapper\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio-bar\\" value=\\"bar\\"></div><label class=\\"radio-label\\" for=\\"radio-bar\\">bar</label>
</div>
</div>"
`;

View File

@ -0,0 +1,2 @@
export { default as VRadio } from "./Radio.vue";
export { default as VRadioGroup } from "./RadioGroup.vue";

View File

@ -42,20 +42,20 @@
<h2 class="mb-1">Size:</h2>
<div class="mb-3">
<VInput
class="mb-2"
v-model="inputValue"
class="mb-2"
placeholder="请输入邮箱"
size="lg"
/>
<VInput
class="mb-2"
v-model="inputValue"
class="mb-2"
placeholder="请输入邮箱"
size="md"
/>
<VInput
class="mb-2"
v-model="inputValue"
class="mb-2"
placeholder="请输入邮箱"
size="sm"
/>
@ -87,7 +87,7 @@
</div>
<h2 class="mb-1">Disabled:</h2>
<div class="mb-3">
<VTextarea v-model="inputValue" disabled :rows="4" />
<VTextarea v-model="inputValue" :rows="4" disabled />
</div>
</section>
<section class="box border-2 rounded p-2">
@ -97,8 +97,8 @@
<VSelect v-model="selectValue">
<VOption
v-for="(option, index) in selectData"
:value="option.value"
:key="index"
:value="option.value"
>
{{ option.label }}
</VOption>
@ -109,14 +109,32 @@
<VSelect v-model="selectValue" disabled>
<VOption
v-for="(option, index) in selectData"
:value="option.value"
:key="index"
:value="option.value"
>
{{ option.label }}
</VOption>
</VSelect>
</div>
</section>
<section class="box border-2 rounded p-2">
<h1 class="text-xl font-bold mb-2">Radio</h1>
<h2 class="mb-1">Radio:</h2>
<div class="mb-3">
<VRadio
v-for="(option, index) in radioData"
:key="index"
v-model="radioValue"
:label="option.label"
:value="option.value"
name="fruit"
></VRadio>
</div>
<h2 class="mb-1">Radio Group:</h2>
<div class="mb-3">
<VRadioGroup v-model="radioValue" :options="radioData"> </VRadioGroup>
</div>
</section>
</div>
</FilledLayout>
</template>
@ -125,12 +143,14 @@
import { FilledLayout } from "../layouts";
import { VButton } from "@/components/base/button";
import { VInput } from "@/components/base/input";
import { VSelect, VOption } from "@/components/base/select";
import { VOption, VSelect } from "@/components/base/select";
import { VTextarea } from "@/components/base/textarea";
import { VRadio, VRadioGroup } from "@/components/base/radio";
import { ref } from "vue";
const inputValue = ref();
const selectValue = ref();
const radioValue = ref("apple");
const selectData = [
{
@ -142,4 +162,19 @@ const selectData = [
label: "2",
},
];
const radioData = [
{
value: "banana",
label: "Banana",
},
{
value: "apple",
label: "Apple",
},
{
value: "orange",
label: "Orange",
},
];
</script>