1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-24 08:04:08 +02:00

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>
This commit is contained in:
かっこかり
2026-01-14 14:02:50 +09:00
committed by GitHub
parent 153ebd4392
commit b941c896aa
34 changed files with 505 additions and 284 deletions

View File

@@ -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 MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
import type { StyleValue } from 'vue';
import type { OptionValue } from '@/types/option-value.js';
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();
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
options = options.filter(vnode => vnode.type !== Comment);
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)] : []),
]);
},
});
export type MkRadiosOption<T = OptionValue, S = string> = {
value: T;
slotId?: S;
label?: string;
labelStyle?: StyleValue;
icon?: string;
iconStyle?: StyleValue;
caption?: string;
disabled?: boolean;
};
</script>
<style lang="scss">
.novjtcto {
> .label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
<script setup lang="ts" generic="const T extends MkRadiosOption">
import MkRadio from './MkRadio.vue';
&:empty {
display: none;
}
}
defineProps<{
options: T[];
vertical?: boolean;
}>();
> .body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
defineSlots<{
label?: () => any;
caption?: () => any;
} & {
[K in `option-${SlotNames}`]: () => any;
}>();
&:empty {
display: none;
}
}
const model = defineModel<T['value']>({ required: true });
&.vertical {
> .body {
flex-direction: column;
}
function getKey(value: OptionValue): PropertyKey {
if (value === null) return 'null';
return value;
}
</script>
<style lang="scss" module>
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
}
}
.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);
&: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);
}
.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>