summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-10-05 18:53:28 +0900
committerGitHub <noreply@github.com>2025-10-05 18:53:28 +0900
commiteae9af73c28dbfcc8681a46a07dc497215f1b283 (patch)
tree29b7ea7f11b57f322986c00679b27b2c2e41cbe1 /packages/frontend/src
parentenhance(frontend): プロフィールバナー画像のparallaxをscroll-driv... (diff)
downloadmisskey-eae9af73c28dbfcc8681a46a07dc497215f1b283.tar.gz
misskey-eae9af73c28dbfcc8681a46a07dc497215f1b283.tar.bz2
misskey-eae9af73c28dbfcc8681a46a07dc497215f1b283.zip
enhance(frontend): MkTabs, MkPageHeader.tabsにてタブハイライトのCSS Anchor Positioningに対応 (#16595)
* fix(frontend): modalの中でtabsを使用する際にハイライトが変な位置に出る問題を修正 * fix lint * Revert "fix(frontend): modalの中でtabsを使用する際にハイライトが変な位置に出る問題を修正" This reverts commit 3b0ec46990d9ac9ae7f22140dfed5f8b80a4190c. * fix * fix * enhance(frontend): MkTabsのタブハイライト切り替えをCSS anchor positioningに対応させる * enhance(frontend): MkPageHeader.tabsのタブハイライト切り替えをCSS anchor positioningに対応させる * :art:
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkTabs.vue86
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue70
2 files changed, 113 insertions, 43 deletions
diff --git a/packages/frontend/src/components/MkTabs.vue b/packages/frontend/src/components/MkTabs.vue
index 57fb6548ba..9798e2c3b3 100644
--- a/packages/frontend/src/components/MkTabs.vue
+++ b/packages/frontend/src/components/MkTabs.vue
@@ -4,12 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.tabs, { [$style.centered]: props.centered }]">
+<div :class="[$style.tabs, { [$style.centered]: props.centered }]" :style="{ '--tabAnchorName': tabAnchorName }">
<div :class="$style.tabsInner">
<button
- v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
- class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
- @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
+ v-for="t in tabs"
+ :ref="(el) => tabRefs[t.key] = (el as HTMLElement)"
+ v-tooltip.noDelay="t.title"
+ class="_button"
+ :class="[$style.tab, {
+ [$style.active]: t.key != null && t.key === tab,
+ [$style.animate]: prefer.s.animation,
+ }]"
+ :style="getTabStyle(t)"
+ @mousedown="(ev) => onTabMousedown(t, ev)"
+ @click="(ev) => onTabClick(t, ev)"
>
<div :class="$style.tabInner">
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
@@ -20,7 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ t.title }}
</div>
<Transition
- v-else mode="in-out" @enter="enter" @afterEnter="afterEnter" @leave="leave"
+ v-else
+ mode="in-out"
+ @enter="enter"
+ @afterEnter="afterEnter"
+ @leave="leave"
@afterLeave="afterLeave"
>
<div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
@@ -36,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-export type Tab = {
- key: string;
+export type Tab<K = string> = {
+ key: K;
onClick?: (ev: MouseEvent) => void;
iconOnly?: boolean;
title: string;
@@ -45,31 +57,46 @@ export type Tab = {
};
</script>
-<script lang="ts" setup>
+<script lang="ts" setup generic="const T extends Tab">
import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue';
import { prefer } from '@/preferences.js';
+import { genId } from '@/utility/id.js';
+
+const cssAnchorSupported = CSS.supports('position-anchor', '--anchor-name');
+const tabAnchorName = `--${genId()}-currentTab`;
const props = withDefaults(defineProps<{
- tabs?: Tab[];
- tab?: string;
+ tabs?: T[];
centered?: boolean;
tabHighlightUpper?: boolean;
}>(), {
- tabs: () => ([] as Tab[]),
+ tabs: () => ([] as T[]),
});
const emit = defineEmits<{
- (ev: 'update:tab', key: string);
(ev: 'tabClick', key: string);
}>();
+const tab = defineModel<T['key']>('tab');
+
const tabHighlightEl = useTemplateRef('tabHighlightEl');
const tabRefs: Record<string, HTMLElement | null> = {};
-function onTabMousedown(tab: Tab, ev: MouseEvent): void {
+function getTabStyle(t: Tab): Record<string, string> {
+ if (!cssAnchorSupported) return {};
+ if (t.key === tab.value) {
+ return {
+ anchorName: tabAnchorName,
+ };
+ } else {
+ return {};
+ }
+}
+
+function onTabMousedown(selectedTab: Tab, ev: MouseEvent): void {
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
- if (tab.key) {
- emit('update:tab', tab.key);
+ if (selectedTab.key) {
+ tab.value = selectedTab.key;
}
}
@@ -83,12 +110,14 @@ function onTabClick(t: Tab, ev: MouseEvent): void {
}
if (t.key) {
- emit('update:tab', t.key);
+ tab.value = t.key;
}
}
function renderTab() {
- const tabEl = props.tab ? tabRefs[props.tab] : undefined;
+ if (cssAnchorSupported) return;
+
+ const tabEl = tab.value ? tabRefs[tab.value] : undefined;
if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) {
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
@@ -138,14 +167,14 @@ function afterLeave(el: Element) {
}
onMounted(() => {
- watch([() => props.tab, () => props.tabs], () => {
- nextTick(() => {
- if (entering) return;
- renderTab();
- });
- }, {
- immediate: true,
- });
+ if (!cssAnchorSupported) {
+ watch([tab, () => props.tabs], () => {
+ nextTick(() => {
+ if (entering) return;
+ renderTab();
+ });
+ }, { immediate: true });
+ }
});
onUnmounted(() => {
@@ -238,4 +267,11 @@ onUnmounted(() => {
bottom: auto;
}
}
+
+@supports (position-anchor: --anchor-name) {
+ .tabHighlight {
+ left: anchor(var(--tabAnchorName) start);
+ width: anchor-size(var(--tabAnchorName) width);
+ }
+}
</style>
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index a1b57f30d9..1ef75281fd 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -4,12 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div ref="el" :class="$style.tabs" @wheel="onTabWheel">
+<div ref="el" :class="$style.tabs" :style="{ '--tabAnchorName': tabAnchorName }" @wheel="onTabWheel">
<div :class="$style.tabsInner">
<button
- v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
- class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
- @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
+ v-for="t in tabs"
+ :ref="(el) => tabRefs[t.key] = (el as HTMLElement)"
+ v-tooltip.noDelay="t.title"
+ class="_button"
+ :class="[$style.tab, {
+ [$style.active]: t.key != null && t.key === props.tab,
+ [$style.animate]: prefer.s.animation
+ }]"
+ :style="getTabStyle(t)"
+ @mousedown="(ev) => onTabMousedown(t, ev)"
+ @click="(ev) => onTabClick(t, ev)"
>
<div :class="$style.tabInner">
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
@@ -48,6 +56,10 @@ export type Tab = {
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue';
import { prefer } from '@/preferences.js';
+import { genId } from '@/utility/id.js';
+
+const cssAnchorSupported = CSS.supports('position-anchor', '--anchor-name');
+const tabAnchorName = `--${genId()}-currentTab`;
const props = withDefaults(defineProps<{
tabs?: Tab[];
@@ -66,6 +78,17 @@ const el = useTemplateRef('el');
const tabHighlightEl = useTemplateRef('tabHighlightEl');
const tabRefs: Record<string, HTMLElement | null> = {};
+function getTabStyle(t: Tab) {
+ if (!cssAnchorSupported) return {};
+ if (t.key === props.tab) {
+ return {
+ anchorName: tabAnchorName,
+ };
+ } else {
+ return {};
+ }
+}
+
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
if (tab.key) {
@@ -88,6 +111,8 @@ function onTabClick(t: Tab, ev: MouseEvent): void {
}
function renderTab() {
+ if (cssAnchorSupported) return;
+
const tabEl = props.tab ? tabRefs[props.tab] : undefined;
if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) {
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
@@ -152,22 +177,24 @@ function afterLeave(el: Element) {
let ro2: ResizeObserver | null;
onMounted(() => {
- watch([() => props.tab, () => props.tabs], () => {
- nextTick(() => {
- if (entering) return;
- renderTab();
+ if (!cssAnchorSupported) {
+ watch([() => props.tab, () => props.tabs], () => {
+ nextTick(() => {
+ if (entering) return;
+ renderTab();
+ });
+ }, {
+ immediate: true,
});
- }, {
- immediate: true,
- });
- if (props.rootEl) {
- ro2 = new ResizeObserver((entries, observer) => {
- if (window.document.body.contains(el.value as HTMLElement)) {
- nextTick(() => renderTab());
- }
- });
- ro2.observe(props.rootEl);
+ if (props.rootEl) {
+ ro2 = new ResizeObserver(() => {
+ if (window.document.body.contains(el.value as HTMLElement)) {
+ nextTick(() => renderTab());
+ }
+ });
+ ro2.observe(props.rootEl);
+ }
}
});
@@ -246,4 +273,11 @@ onUnmounted(() => {
transition: width 0.15s ease, left 0.15s ease;
}
}
+
+@supports (position-anchor: --anchor-name) {
+ .tabHighlight {
+ left: anchor(var(--tabAnchorName) start);
+ width: anchor-size(var(--tabAnchorName) width);
+ }
+}
</style>