Skip to content

Commit 3bfe6d7

Browse files
committed
fix: align active category chip with fully visible cards
Update category activation to prioritize sections that have at least one fully visible card inside the effective viewport (accounting for the sticky chip bar offset). Keep geometry fallback for edge cases and add unit tests for the new selection heuristic. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent df67660 commit 3bfe6d7

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

src/tests/views/Settings/PolicyWorkbench/useNavigation.spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { mount } from '@vue/test-utils'
77
import { defineComponent, h } from 'vue'
88
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9-
import { useNavigation } from '../../../../views/Settings/PolicyWorkbench/Catalog/composables/useNavigation'
9+
import { pickCategoryWithFullyVisibleCard, useNavigation } from '../../../../views/Settings/PolicyWorkbench/Catalog/composables/useNavigation'
1010

1111
function createRect(top: number): DOMRect {
1212
return {
@@ -23,6 +23,47 @@ function createRect(top: number): DOMRect {
2323
}
2424

2525
describe('useNavigation', () => {
26+
it('prefers the section with a fully visible card over one with only partial cards', () => {
27+
const selected = pickCategoryWithFullyVisibleCard([
28+
{
29+
key: 'signer-experience',
30+
cardRects: [{ top: 80, bottom: 150 }],
31+
},
32+
{
33+
key: 'what-gets-recorded',
34+
cardRects: [{ top: 170, bottom: 240 }],
35+
},
36+
], 160, 320)
37+
38+
expect(selected).toBe('what-gets-recorded')
39+
})
40+
41+
it('accounts for sticky chips area by requiring card top below viewport top cutoff', () => {
42+
const selected = pickCategoryWithFullyVisibleCard([
43+
{
44+
key: 'signer-experience',
45+
cardRects: [{ top: 110, bottom: 180 }],
46+
},
47+
{
48+
key: 'what-gets-recorded',
49+
cardRects: [{ top: 142, bottom: 212 }],
50+
},
51+
], 140, 320)
52+
53+
expect(selected).toBe('what-gets-recorded')
54+
})
55+
56+
it('returns null when no card is fully visible', () => {
57+
const selected = pickCategoryWithFullyVisibleCard([
58+
{
59+
key: 'what-gets-recorded',
60+
cardRects: [{ top: 130, bottom: 230 }],
61+
},
62+
], 150, 220)
63+
64+
expect(selected).toBeNull()
65+
})
66+
2667
function createHarness() {
2768
const Harness = defineComponent({
2869
setup(_, { expose }) {

src/views/Settings/PolicyWorkbench/Catalog/composables/useNavigation.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,41 @@ const CATEGORY_SCROLL_ALIGNMENT_GAP_PX = 12
1313
const SECTION_OBSERVER_BOTTOM_MARGIN_PERCENT = 45
1414
const NAVIGATION_LOCK_MS = 900
1515

16+
type RectBounds = {
17+
top: number
18+
bottom: number
19+
}
20+
21+
type CategoryCardVisibility = {
22+
key: RealPolicySettingCategory
23+
cardRects: RectBounds[]
24+
}
25+
26+
export function pickCategoryWithFullyVisibleCard(
27+
categories: CategoryCardVisibility[],
28+
viewportTop: number,
29+
viewportBottom: number,
30+
): RealPolicySettingCategory | null {
31+
let selectedCategory: RealPolicySettingCategory | null = null
32+
let selectedTop = Number.POSITIVE_INFINITY
33+
34+
for (const category of categories) {
35+
for (const rect of category.cardRects) {
36+
const isFullyVisible = rect.top >= viewportTop && rect.bottom <= viewportBottom && rect.bottom > rect.top
37+
if (!isFullyVisible) {
38+
continue
39+
}
40+
41+
if (rect.top < selectedTop) {
42+
selectedTop = rect.top
43+
selectedCategory = category.key
44+
}
45+
}
46+
}
47+
48+
return selectedCategory
49+
}
50+
1651
export function useNavigation(
1752
visibleCategorySections: { value: Array<{ key: RealPolicySettingCategory }> },
1853
) {
@@ -115,7 +150,60 @@ export function useNavigation(
115150
return Math.max(0, Math.round(activationLine - containerRect.top))
116151
}
117152

153+
function categoryDetectionViewportBounds(): RectBounds {
154+
const top = categoryActivationLinePx() + CATEGORY_SCROLL_ALIGNMENT_GAP_PX
155+
if (scrollContainer.value instanceof Window) {
156+
return { top, bottom: window.innerHeight }
157+
}
158+
159+
if (typeof scrollContainer.value.getBoundingClientRect === 'function') {
160+
return {
161+
top,
162+
bottom: scrollContainer.value.getBoundingClientRect().bottom,
163+
}
164+
}
165+
166+
return { top, bottom: top + 1 }
167+
}
168+
169+
function collectCategoryCardRects(): CategoryCardVisibility[] {
170+
const categories: CategoryCardVisibility[] = []
171+
172+
for (const section of visibleCategorySectionsValue.value) {
173+
const sectionElement = categorySectionElements.get(section.key)
174+
if (!sectionElement) {
175+
continue
176+
}
177+
178+
const cards = sectionElement.querySelectorAll<HTMLElement>('.policy-workbench__setting-tile, .policy-workbench__settings-row')
179+
const cardRects = Array.from(cards).map((card) => {
180+
const rect = card.getBoundingClientRect()
181+
return {
182+
top: rect.top,
183+
bottom: rect.bottom,
184+
}
185+
})
186+
187+
categories.push({
188+
key: section.key,
189+
cardRects,
190+
})
191+
}
192+
193+
return categories
194+
}
195+
118196
function pickCategoryByGeometry(): RealPolicySettingCategory {
197+
const viewport = categoryDetectionViewportBounds()
198+
const byFullyVisibleCard = pickCategoryWithFullyVisibleCard(
199+
collectCategoryCardRects(),
200+
viewport.top,
201+
viewport.bottom,
202+
)
203+
if (byFullyVisibleCard) {
204+
return byFullyVisibleCard
205+
}
206+
119207
const activationLine = categoryActivationLinePx()
120208
let lastPassedIndex = -1
121209

0 commit comments

Comments
 (0)