mirror of https://github.com/halo-dev/halo
feat: add radio and radio-group component
Signed-off-by: Ryan Wang <i@ryanc.cc>pull/3445/head
parent
195fb87552
commit
717ef1da35
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>"
|
||||||
|
`;
|
|
@ -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>"
|
||||||
|
`;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as VRadio } from "./Radio.vue";
|
||||||
|
export { default as VRadioGroup } from "./RadioGroup.vue";
|
|
@ -42,20 +42,20 @@
|
||||||
<h2 class="mb-1">Size:</h2>
|
<h2 class="mb-1">Size:</h2>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<VInput
|
<VInput
|
||||||
class="mb-2"
|
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
|
class="mb-2"
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱"
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
<VInput
|
<VInput
|
||||||
class="mb-2"
|
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
|
class="mb-2"
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱"
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<VInput
|
<VInput
|
||||||
class="mb-2"
|
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
|
class="mb-2"
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mb-1">Disabled:</h2>
|
<h2 class="mb-1">Disabled:</h2>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<VTextarea v-model="inputValue" disabled :rows="4" />
|
<VTextarea v-model="inputValue" :rows="4" disabled />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="box border-2 rounded p-2">
|
<section class="box border-2 rounded p-2">
|
||||||
|
@ -97,8 +97,8 @@
|
||||||
<VSelect v-model="selectValue">
|
<VSelect v-model="selectValue">
|
||||||
<VOption
|
<VOption
|
||||||
v-for="(option, index) in selectData"
|
v-for="(option, index) in selectData"
|
||||||
:value="option.value"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</VOption>
|
</VOption>
|
||||||
|
@ -109,14 +109,32 @@
|
||||||
<VSelect v-model="selectValue" disabled>
|
<VSelect v-model="selectValue" disabled>
|
||||||
<VOption
|
<VOption
|
||||||
v-for="(option, index) in selectData"
|
v-for="(option, index) in selectData"
|
||||||
:value="option.value"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</VOption>
|
</VOption>
|
||||||
</VSelect>
|
</VSelect>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
</FilledLayout>
|
</FilledLayout>
|
||||||
</template>
|
</template>
|
||||||
|
@ -125,12 +143,14 @@
|
||||||
import { FilledLayout } from "../layouts";
|
import { FilledLayout } from "../layouts";
|
||||||
import { VButton } from "@/components/base/button";
|
import { VButton } from "@/components/base/button";
|
||||||
import { VInput } from "@/components/base/input";
|
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 { VTextarea } from "@/components/base/textarea";
|
||||||
|
import { VRadio, VRadioGroup } from "@/components/base/radio";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const inputValue = ref();
|
const inputValue = ref();
|
||||||
const selectValue = ref();
|
const selectValue = ref();
|
||||||
|
const radioValue = ref("apple");
|
||||||
|
|
||||||
const selectData = [
|
const selectData = [
|
||||||
{
|
{
|
||||||
|
@ -142,4 +162,19 @@ const selectData = [
|
||||||
label: "2",
|
label: "2",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const radioData = [
|
||||||
|
{
|
||||||
|
value: "banana",
|
||||||
|
label: "Banana",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "apple",
|
||||||
|
label: "Apple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "orange",
|
||||||
|
label: "Orange",
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue