fix(web5): bound connected-nodes tab panes to sibling-matched height (UIFIX-02)
All checks were successful
Demo images / Build & push demo images (push) Successful in 3m58s
All checks were successful
Demo images / Build & push demo images (push) Successful in 3m58s
The three tab panes carried max-h-72 xl:max-h-none, so at the xl breakpoint (where the Web5 row becomes two grid columns) the cap lifted with nothing to replace it: the visible pane grew to fit every row, stretched the grid row, and the scrollbar the user expects never appeared. Give each pane xl:flex-1 xl:basis-0 xl:max-h-none instead — zero flex-basis means the pane contributes no intrinsic height, so the grid row is sized by the Web5NodeVisibility sibling alone, grid's default align-items: stretch gives the card that height, and flex-1 hands the leftover height back to the pane, which scrolls inside it via the existing overflow-y-auto. The card root gets min-h-0 (so the flex column can shrink below content height) plus an xl:min-h-[20rem] floor so a short sibling still leaves a usable list area instead of collapsing to the header+tabs strip. Below the row breakpoint nothing changes: the stacked cap (max-h-72) and scroll are untouched, and dropping flex-auto (replaced by nothing, i.e. the default 0 1 auto) has no visible effect since single-column stacked cards have no extra flex space to distribute anyway. Adds Web5ConnectedNodesScroll.test.ts to pin the contract across all three tab panes and the card root so a future cleanup cannot reintroduce the grow-to-fit regression a third time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf9cfc446a
commit
ceafbcb596
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- Connected Nodes (P2P over Tor) -->
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 scroll-mt-24 flex flex-col">
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 scroll-mt-24 flex flex-col min-h-0 xl:min-h-[20rem]">
|
||||
<!-- Desktop: side-by-side layout -->
|
||||
<div class="hidden md:flex items-start gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
@ -54,7 +54,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Trusted tab -->
|
||||
<div v-show="nodesContainerTab === 'trusted'" class="space-y-2 flex-auto min-h-0 overflow-y-auto max-h-72 xl:max-h-none">
|
||||
<div v-show="nodesContainerTab === 'trusted'" class="space-y-2 min-h-0 overflow-y-auto max-h-72 xl:flex-1 xl:basis-0 xl:max-h-none">
|
||||
<div v-if="loadingPeers && peers.length === 0" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
@ -87,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Observers tab -->
|
||||
<div v-show="nodesContainerTab === 'observers'" class="space-y-2 flex-auto min-h-0 overflow-y-auto max-h-72 xl:max-h-none">
|
||||
<div v-show="nodesContainerTab === 'observers'" class="space-y-2 min-h-0 overflow-y-auto max-h-72 xl:flex-1 xl:basis-0 xl:max-h-none">
|
||||
<div v-if="loadingPeers && observers.length === 0" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
@ -117,7 +117,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Requests tab -->
|
||||
<div v-show="nodesContainerTab === 'requests'" class="space-y-2 flex-auto min-h-0 overflow-y-auto max-h-72 xl:max-h-none">
|
||||
<div v-show="nodesContainerTab === 'requests'" class="space-y-2 min-h-0 overflow-y-auto max-h-72 xl:flex-1 xl:basis-0 xl:max-h-none">
|
||||
<div v-if="loadingRequests && connectionRequests.length === 0" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Web5ConnectedNodes from '../Web5ConnectedNodes.vue'
|
||||
|
||||
// Structural pin for UIFIX-02: the connected-nodes card's height must track
|
||||
// its row sibling (via grid `align-items: stretch` + a zero-basis flex
|
||||
// child), and the tab panes must scroll inside that height rather than
|
||||
// growing to fit every row. See:
|
||||
// .planning/todos/pending/2026-07-30-connected-nodes-list-must-scroll-at-row-matched-height.md
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
listPeers: vi.fn(() => new Promise(() => {})),
|
||||
federationListNodes: vi.fn().mockResolvedValue({ nodes: [] }),
|
||||
checkPeerReachable: vi.fn().mockResolvedValue({ reachable: false }),
|
||||
call: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useMessageToast', () => ({
|
||||
useMessageToast: () => ({
|
||||
loadReceivedMessages: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/web5Badge', () => ({
|
||||
useWeb5BadgeStore: () => ({ pendingRequestCount: 0 }),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ peerHealth: {} }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useModalKeyboard', () => ({
|
||||
useModalKeyboard: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('Web5ConnectedNodes scroll contract (UIFIX-02)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('gives all three tab panes the bounded, sibling-matched scroll contract', () => {
|
||||
const wrapper = mount(Web5ConnectedNodes)
|
||||
|
||||
// The three v-show panes (trusted, observers, requests) are always in the
|
||||
// DOM regardless of which tab is active. The Send Message modal is
|
||||
// v-if="false" by default, so this selector matches exactly the panes.
|
||||
const panes = wrapper.findAll('.overflow-y-auto')
|
||||
expect(panes).toHaveLength(3)
|
||||
|
||||
for (const pane of panes) {
|
||||
const classes = pane.classes()
|
||||
// Below the row breakpoint: unchanged stacked cap, still scrollable.
|
||||
expect(classes).toContain('min-h-0')
|
||||
expect(classes).toContain('overflow-y-auto')
|
||||
expect(classes).toContain('max-h-72')
|
||||
// At the row breakpoint: zero-basis growing flex child with no cap —
|
||||
// this is what lets the sibling drive the card's height while the
|
||||
// pane itself contributes nothing to intrinsic sizing.
|
||||
expect(classes).toContain('xl:flex-1')
|
||||
expect(classes).toContain('xl:basis-0')
|
||||
expect(classes).toContain('xl:max-h-none')
|
||||
// The old grow-to-fit-content class must be gone.
|
||||
expect(classes).not.toContain('flex-auto')
|
||||
}
|
||||
})
|
||||
|
||||
it('gives the card root a min-h-0 flex column with a row-breakpoint height floor', () => {
|
||||
const wrapper = mount(Web5ConnectedNodes)
|
||||
|
||||
const root = wrapper.find('[data-controller-container]')
|
||||
const classes = root.classes()
|
||||
|
||||
expect(classes).toContain('flex')
|
||||
expect(classes).toContain('flex-col')
|
||||
expect(classes).toContain('min-h-0')
|
||||
expect(classes).toContain('xl:min-h-[20rem]')
|
||||
})
|
||||
|
||||
it('still renders the empty-state row for each pane when the node list is empty', () => {
|
||||
const wrapper = mount(Web5ConnectedNodes)
|
||||
|
||||
// Default state: no cached peers, observers, or connection requests.
|
||||
expect(wrapper.text()).toContain('web5.noPeers')
|
||||
expect(wrapper.text()).toContain('web5.noObservers')
|
||||
expect(wrapper.text()).toContain('web5.noRequests')
|
||||
|
||||
// The panes themselves are still present in the tree (not collapsed away).
|
||||
expect(wrapper.findAll('.overflow-y-auto')).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user