diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue index c28d02cfcefd4..a1a36a78e69e7 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -159,7 +159,7 @@ ref="resultsContainer" class="unified-search-modal__results" :class="{ 'unified-search-modal__results--held': heldHeight !== null }" - :style="heldHeight !== null ? { blockSize: `${heldHeight}px`, boxSizing: 'border-box' } : undefined"> + :style="heldHeight !== null ? { minBlockSize: `${heldHeight}px`, boxSizing: 'border-box' } : undefined">

{{ t('core', 'Results') }}

@@ -1807,20 +1807,30 @@ export default defineComponent({ min-height: 0; overflow: hidden auto; - // The placeholders deliberately overfill, so the bottom fades out over the cut. + // The reserved height is a floor, not a size. Column layout so the placeholders can + // take what the results leave. &--held { + display: flex; + flex-direction: column; flex-grow: 0; - overflow: clip; - // Capped, so a short box does not spend a third of itself fading. - mask-image: linear-gradient(to bottom, #000 calc(100% - min(2lh, 25%)), transparent); + + > *:not(.search-result-skeleton) { + flex: none; + } } // Adjust padding to match container but keep the scrollbar on the very end padding-inline: calc(var(--default-grid-baseline) * 4); padding-block: 0 calc(var(--default-grid-baseline) * 4); - // Matches the gap a category title keeps above itself. .search-result-skeleton { + // Matches the gap a category title keeps above itself. margin-block-start: 14px; + // The placeholders deliberately overfill, so the bottom fades out over the cut. + flex: 1 1 0; + min-block-size: 0; + overflow: clip; + // Capped, so a short box does not spend a third of itself fading. + mask-image: linear-gradient(to bottom, #000 calc(100% - min(2lh, 25%)), transparent); } .result { @@ -1891,7 +1901,7 @@ export default defineComponent({ // Ensure modal is accessible on small devices @media only screen and (max-height: 400px) { - .unified-search-modal__results:not(.unified-search-modal__results--held) { + .unified-search-modal__results { overflow: unset; } } diff --git a/core/src/tests/components/UnifiedSearchModal.spec.ts b/core/src/tests/components/UnifiedSearchModal.spec.ts index 604f1a5e10c39..1c6597b671778 100644 --- a/core/src/tests/components/UnifiedSearchModal.spec.ts +++ b/core/src/tests/components/UnifiedSearchModal.spec.ts @@ -1267,13 +1267,35 @@ describe('UnifiedSearchModal loading skeleton', () => { expect(wrapper.vm.skeletonRows * 60).toBeGreaterThanOrEqual(300) }) + // isBusy stays true until the last provider answers, so the box carries real results meanwhile. + it('reserves the height as a floor, so landed results are never cut off', async () => { + const wrapper = await withResults() + measureResultsAt(wrapper, 300) + wrapper.vm.searchQuery = 'querying' + await wrapper.vm.$nextTick() + wrapper.vm.find('querying') + + searchStates.value = { + files: loaded([{ resourceUrl: '/a' }, { resourceUrl: '/b' }, { resourceUrl: '/c' }]), + talk: { status: 'loading', entries: [], cursor: null, hasMore: false, loadMoreFailed: false }, + } + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isBusy).toBe(true) + expect(wrapper.vm.hasVisibleResults).toBe(true) + + const style = wrapper.find('.unified-search-modal__results').attributes('style') + expect(style).toContain('min-block-size: 300px') + expect(style).not.toMatch(/(^|[^-])block-size: 300px/) + }) + it('holds the same height through repeated keystrokes rather than creeping taller', async () => { const wrapper = await withResults() const box = wrapper.vm.$refs.resultsContainer as HTMLElement // Stand in for the browser: report the padding on top of whatever height is set. const padding = 16 vi.spyOn(box, 'getBoundingClientRect').mockImplementation(() => ({ - height: (parseFloat(box.style.blockSize) || 300) + (box.style.boxSizing === 'border-box' ? 0 : padding), + height: (parseFloat(box.style.minBlockSize) || 300) + (box.style.boxSizing === 'border-box' ? 0 : padding), }) as DOMRect) wrapper.vm.searchQuery = 'q1'