summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkRadios.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components/MkRadios.vue')
-rw-r--r--packages/frontend/src/components/MkRadios.vue185
1 files changed, 107 insertions, 78 deletions
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 426a1d2c2b..43957a0673 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -3,99 +3,128 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
+<template>
+<div :class="{ [$style.vertical]: vertical }">
+ <div :class="$style.label">
+ <slot name="label"></slot>
+ </div>
+ <div :class="$style.body">
+ <MkRadio
+ v-for="option in options"
+ :key="getKey(option.value)"
+ v-model="model"
+ :disabled="option.disabled"
+ :value="option.value"
+ >
+ <div :class="[$style.optionContent, { [$style.checked]: model === option.value }]">
+ <i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
+ <div>
+ <slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
+ <template v-else>
+ <div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
+ <div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
+ </template>
+ </div>
+ </div>
+ </MkRadio>
+ </div>
+ <div :class="$style.caption">
+ <slot name="caption"></slot>
+ </div>
+</div>
+</template>
+
<script lang="ts">
-import { Comment, defineComponent, h, ref, watch } from 'vue';
+import type { StyleValue } from 'vue';
+import type { OptionValue } from '@/types/option-value.js';
+
+export type MkRadiosOption<T = OptionValue, S = string> = {
+ value: T;
+ slotId?: S;
+ label?: string;
+ labelStyle?: StyleValue;
+ icon?: string;
+ iconStyle?: StyleValue;
+ caption?: string;
+ disabled?: boolean;
+};
+</script>
+
+<script setup lang="ts" generic="const T extends MkRadiosOption">
import MkRadio from './MkRadio.vue';
-import type { VNode } from 'vue';
-export default defineComponent({
- props: {
- modelValue: {
- required: false,
- },
- vertical: {
- type: Boolean,
- default: false,
- },
- },
- setup(props, context) {
- const value = ref(props.modelValue);
- watch(value, () => {
- context.emit('update:modelValue', value.value);
- });
- watch(() => props.modelValue, v => {
- value.value = v;
- });
- if (!context.slots.default) return null;
- let options = context.slots.default();
- const label = context.slots.label && context.slots.label();
- const caption = context.slots.caption && context.slots.caption();
+defineProps<{
+ options: T[];
+ vertical?: boolean;
+}>();
+
+type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
- // なぜかFragmentになることがあるため
- if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
+defineSlots<{
+ label?: () => any;
+ caption?: () => any;
+} & {
+ [K in `option-${SlotNames}`]: () => any;
+}>();
- // vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
- options = options.filter(vnode => vnode.type !== Comment);
+const model = defineModel<T['value']>({ required: true });
- return () => h('div', {
- class: [
- 'novjtcto',
- ...(props.vertical ? ['vertical'] : []),
- ],
- }, [
- ...(label ? [h('div', {
- class: 'label',
- }, label)] : []),
- h('div', {
- class: 'body',
- }, options.map(option => h(MkRadio, {
- key: option.key as string,
- value: option.props?.value,
- disabled: option.props?.disabled,
- modelValue: value.value,
- 'onUpdate:modelValue': _v => value.value = _v,
- }, () => option.children)),
- ),
- ...(caption ? [h('div', {
- class: 'caption',
- }, caption)] : []),
- ]);
- },
-});
+function getKey(value: OptionValue): PropertyKey {
+ if (value === null) return 'null';
+ return value;
+}
</script>
-<style lang="scss">
-.novjtcto {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 0;
- user-select: none;
+<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .body {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- }
+.body {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
- > .caption {
- font-size: 0.85em;
- padding: 8px 0 0 0;
- color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
+
+.optionContent {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.optionCaption {
+ font-size: 0.85em;
+ padding: 2px 0 0 0;
+ color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
+}
- &.vertical {
- > .body {
- flex-direction: column;
- }
+.optionContent.checked {
+ .optionCaption {
+ color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
}
}
+
+.optionIcon {
+ flex-shrink: 0;
+}
+
+.vertical > .body {
+ flex-direction: column;
+}
</style>