diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2026-01-14 14:02:50 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-14 14:02:50 +0900 |
| commit | b941c896aa5512240de9121a1850d55aa5f8b68b (patch) | |
| tree | 5d96055387b458f5295d791cc00fd5abf14b1752 /packages/frontend/src/components/MkRadios.vue | |
| parent | Bump version to 2026.1.0-beta.0 (diff) | |
| download | misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.tar.gz misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.tar.bz2 misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.zip | |
refactor(frontend): MkRadiosの指定をpropsから行うように (#16597)
* refactor(frontend): MkRadiosの指定をpropsから行うように
* spdx
* fix lint
* fix: mkradiosを動的slotsに対応させる
* fix: remove comment [ci skip]
* fix lint
* fix lint
* migrate
* rename
* fix
* fix
* fix types
* remove unused imports
* fix
* wip
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
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> |