diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-10-05 18:53:28 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-05 18:53:28 +0900 |
| commit | eae9af73c28dbfcc8681a46a07dc497215f1b283 (patch) | |
| tree | 29b7ea7f11b57f322986c00679b27b2c2e41cbe1 /packages/frontend/src | |
| parent | enhance(frontend): プロフィールバナー画像のparallaxをscroll-driv... (diff) | |
| download | misskey-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.vue | 86 | ||||
| -rw-r--r-- | packages/frontend/src/components/global/MkPageHeader.tabs.vue | 70 |
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> |