Stop reserving layout space for the idle sync spinner

visibility:hidden still occupied the header's flex row, squeezing the
title on narrow screens and clipping the unread counter. Render the
spinner with v-if instead so it takes no space when not syncing.
This commit is contained in:
2026-09-25 13:55:23 +02:00
parent e51bce4244
commit 8f7ddb77c4
2 changed files with 18 additions and 15 deletions
+2 -7
View File
@@ -162,8 +162,8 @@ function handleToggleLayout() {
<div class="app-nav__wrapper"> <div class="app-nav__wrapper">
<span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span> <span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span>
<span <span
v-if="isSyncing"
class="app-nav__sync-spinner" class="app-nav__sync-spinner"
:class="{ 'app-nav__sync-spinner--active': isSyncing }"
aria-hidden="true" aria-hidden="true"
></span> ></span>
<select <select
@@ -268,11 +268,6 @@ function handleToggleLayout() {
border-radius: 50%; border-radius: 50%;
opacity: 0.6; opacity: 0.6;
flex-shrink: 0; flex-shrink: 0;
visibility: hidden;
}
.app-nav__sync-spinner--active {
visibility: visible;
animation: app-nav-spin 0.8s linear infinite; animation: app-nav-spin 0.8s linear infinite;
} }
@@ -283,7 +278,7 @@ function handleToggleLayout() {
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.app-nav__sync-spinner--active { .app-nav__sync-spinner {
animation: none; animation: none;
} }
} }
+16 -8
View File
@@ -311,16 +311,14 @@ describe('AppNav', () => {
}) })
describe('sync spinner', () => { describe('sync spinner', () => {
it('always renders the spinner, inactive, when idle', async () => { it('does not render the spinner, and reserves no space for it, when idle', async () => {
const wrapper = mountNav() const wrapper = mountNav()
await flushPromises() await flushPromises()
const spinner = wrapper.find('.app-nav__sync-spinner') expect(wrapper.find('.app-nav__sync-spinner').exists()).toBe(false)
expect(spinner.exists()).toBe(true)
expect(spinner.classes()).not.toContain('app-nav__sync-spinner--active')
}) })
it('activates while a sync triggered through the composable is in flight, and deactivates once it resolves', async () => { it('renders while a sync triggered through the composable is in flight, and disappears once it resolves', async () => {
const wrapper = mountNav() const wrapper = mountNav()
await flushPromises() await flushPromises()
@@ -330,25 +328,35 @@ describe('AppNav', () => {
const syncPromise = sync(true) const syncPromise = sync(true)
await nextTick() await nextTick()
expect(wrapper.find('.app-nav__sync-spinner').classes()).toContain('app-nav__sync-spinner--active') expect(wrapper.find('.app-nav__sync-spinner').exists()).toBe(true)
axios.get.mockResolvedValueOnce({ data: { feeds: [] } }) axios.get.mockResolvedValueOnce({ data: { feeds: [] } })
resolvePost({ status: 200 }) resolvePost({ status: 200 })
await syncPromise await syncPromise
await nextTick() await nextTick()
expect(wrapper.find('.app-nav__sync-spinner').classes()).not.toContain('app-nav__sync-spinner--active') expect(wrapper.find('.app-nav__sync-spinner').exists()).toBe(false)
}) })
it('renders even on a page where the feed filter itself does not render', async () => { it('renders while syncing even on a page where the feed filter itself does not render', async () => {
await router.push('/login') await router.push('/login')
await router.isReady() await router.isReady()
const wrapper = mountNav() const wrapper = mountNav()
await flushPromises() await flushPromises()
let resolvePost
axios.post.mockReturnValueOnce(new Promise(resolve => { resolvePost = resolve }))
const { sync } = useFeeds()
const syncPromise = sync(true)
await nextTick()
expect(wrapper.find('.app-nav__filter').exists()).toBe(false) expect(wrapper.find('.app-nav__filter').exists()).toBe(false)
expect(wrapper.find('.app-nav__sync-spinner').exists()).toBe(true) expect(wrapper.find('.app-nav__sync-spinner').exists()).toBe(true)
axios.get.mockResolvedValueOnce({ data: { feeds: [] } })
resolvePost({ status: 200 })
await syncPromise
}) })
}) })