diff options
Diffstat (limited to 'packages/frontend/src/components/MkRadios.vue')
| -rw-r--r-- | packages/frontend/src/components/MkRadios.vue | 185 |
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> |