Skip to content

Commit 68abc5d

Browse files
refactor: add new useVisibleItems composable (#2395)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9bd0c4d commit 68abc5d

File tree

3 files changed

+203
-17
lines changed

3 files changed

+203
-17
lines changed

app/components/Package/Dependencies.vue

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ function getDeprecatedDepInfo(depName: string) {
3737
return vulnTree.value.deprecatedPackages.find(p => p.name === depName && p.depth === 'direct')
3838
}
3939
40-
// Expanded state for each section
41-
const depsExpanded = shallowRef(false)
42-
const peerDepsExpanded = shallowRef(false)
43-
const optionalDepsExpanded = shallowRef(false)
44-
4540
// Sort dependencies alphabetically
4641
const sortedDependencies = computed(() => {
4742
if (!props.dependencies) return []
@@ -89,6 +84,24 @@ function getDepVersionClass(dep: string) {
8984
return getVersionClass(undefined)
9085
}
9186
87+
const {
88+
visibleItems: visibleDeps,
89+
hasMore: hasMoreDeps,
90+
expand: expandDeps,
91+
} = useVisibleItems(sortedDependencies, 10)
92+
93+
const {
94+
visibleItems: visiblePeerDeps,
95+
hasMore: hasMorePeerDeps,
96+
expand: expandPeerDeps,
97+
} = useVisibleItems(sortedPeerDependencies, 10)
98+
99+
const {
100+
visibleItems: visibleOptionalDeps,
101+
hasMore: hasMoreOptionalDeps,
102+
expand: expandOptionalDeps,
103+
} = useVisibleItems(sortedOptionalDependencies, 10)
104+
92105
const numberFormatter = useNumberFormatter()
93106
</script>
94107

@@ -110,7 +123,7 @@ const numberFormatter = useNumberFormatter()
110123
>
111124
<ul class="space-y-1 list-none m-0" :aria-label="$t('package.dependencies.list_label')">
112125
<li
113-
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
126+
v-for="[dep, version] in visibleDeps"
114127
:key="dep"
115128
class="flex items-center justify-between py-1 text-sm gap-2"
116129
>
@@ -190,10 +203,10 @@ const numberFormatter = useNumberFormatter()
190203
</li>
191204
</ul>
192205
<button
193-
v-if="sortedDependencies.length > 10 && !depsExpanded"
206+
v-if="hasMoreDeps"
194207
type="button"
195208
class="my-2 ms-1 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70"
196-
@click="depsExpanded = true"
209+
@click="expandDeps"
197210
>
198211
{{
199212
$t(
@@ -222,7 +235,7 @@ const numberFormatter = useNumberFormatter()
222235
:aria-label="$t('package.peer_dependencies.list_label')"
223236
>
224237
<li
225-
v-for="peer in sortedPeerDependencies.slice(0, peerDepsExpanded ? undefined : 10)"
238+
v-for="peer in visiblePeerDeps"
226239
:key="peer.name"
227240
class="flex items-center justify-between py-1 text-sm gap-1 min-w-0"
228241
>
@@ -245,10 +258,10 @@ const numberFormatter = useNumberFormatter()
245258
</li>
246259
</ul>
247260
<button
248-
v-if="sortedPeerDependencies.length > 10 && !peerDepsExpanded"
261+
v-if="hasMorePeerDeps"
249262
type="button"
250263
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70"
251-
@click="peerDepsExpanded = true"
264+
@click="expandPeerDeps"
252265
>
253266
{{
254267
$t(
@@ -281,10 +294,7 @@ const numberFormatter = useNumberFormatter()
281294
:aria-label="$t('package.optional_dependencies.list_label')"
282295
>
283296
<li
284-
v-for="[dep, version] in sortedOptionalDependencies.slice(
285-
0,
286-
optionalDepsExpanded ? undefined : 10,
287-
)"
297+
v-for="[dep, version] in visibleOptionalDeps"
288298
:key="dep"
289299
class="flex items-baseline justify-between py-1 text-sm gap-2"
290300
>
@@ -302,10 +312,10 @@ const numberFormatter = useNumberFormatter()
302312
</li>
303313
</ul>
304314
<button
305-
v-if="sortedOptionalDependencies.length > 10 && !optionalDepsExpanded"
315+
v-if="hasMoreOptionalDeps"
306316
type="button"
307317
class="mt-2 truncate"
308-
@click="optionalDepsExpanded = true"
318+
@click="expandOptionalDeps"
309319
>
310320
{{
311321
$t(

app/composables/useVisibleItems.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { computed, shallowRef, toValue } from 'vue'
2+
import type { MaybeRefOrGetter } from 'vue'
3+
4+
export function useVisibleItems<T>(items: MaybeRefOrGetter<T[]>, limit: number) {
5+
const showAll = shallowRef(false)
6+
7+
const visibleItems = computed(() => {
8+
const list = toValue(items)
9+
return showAll.value ? list : list.slice(0, limit)
10+
})
11+
12+
const hiddenCount = computed(() =>
13+
showAll.value ? 0 : Math.max(0, toValue(items).length - limit),
14+
)
15+
16+
const hasMore = computed(() => !showAll.value && toValue(items).length > limit)
17+
18+
const expand = () => {
19+
showAll.value = true
20+
}
21+
const collapse = () => {
22+
showAll.value = false
23+
}
24+
const toggle = () => {
25+
showAll.value = !showAll.value
26+
}
27+
28+
return { visibleItems, hiddenCount, hasMore, showAll, expand, collapse, toggle }
29+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { computed, ref } from 'vue'
3+
import { useVisibleItems } from '~/composables/useVisibleItems'
4+
5+
describe('useVisibleItems', () => {
6+
describe('initial state', () => {
7+
it('returns all items when list is within limit', () => {
8+
const { visibleItems, hasMore, hiddenCount } = useVisibleItems(['a', 'b', 'c'], 5)
9+
10+
expect(visibleItems.value).toEqual(['a', 'b', 'c'])
11+
expect(hasMore.value).toBe(false)
12+
expect(hiddenCount.value).toBe(0)
13+
})
14+
15+
it('returns exactly limit items when list exceeds limit', () => {
16+
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
17+
const { visibleItems, hasMore, hiddenCount } = useVisibleItems(items, 5)
18+
19+
expect(visibleItems.value).toEqual([1, 2, 3, 4, 5])
20+
expect(hasMore.value).toBe(true)
21+
expect(hiddenCount.value).toBe(5)
22+
})
23+
24+
it('returns all items when list length equals limit exactly', () => {
25+
const items = [1, 2, 3]
26+
const { visibleItems, hasMore, hiddenCount } = useVisibleItems(items, 3)
27+
28+
expect(visibleItems.value).toEqual([1, 2, 3])
29+
expect(hasMore.value).toBe(false)
30+
expect(hiddenCount.value).toBe(0)
31+
})
32+
33+
it('handles empty list', () => {
34+
const { visibleItems, hasMore, hiddenCount } = useVisibleItems([], 5)
35+
36+
expect(visibleItems.value).toEqual([])
37+
expect(hasMore.value).toBe(false)
38+
expect(hiddenCount.value).toBe(0)
39+
})
40+
})
41+
42+
describe('expand()', () => {
43+
it('shows all items after expand', () => {
44+
const items = [1, 2, 3, 4, 5, 6]
45+
const { visibleItems, hasMore, hiddenCount, expand } = useVisibleItems(items, 3)
46+
47+
expect(visibleItems.value).toEqual([1, 2, 3])
48+
49+
expand()
50+
51+
expect(visibleItems.value).toEqual([1, 2, 3, 4, 5, 6])
52+
expect(hasMore.value).toBe(false)
53+
expect(hiddenCount.value).toBe(0)
54+
})
55+
})
56+
57+
describe('collapse()', () => {
58+
it('hides items again after collapse', () => {
59+
const items = [1, 2, 3, 4, 5, 6]
60+
const { visibleItems, hasMore, expand, collapse } = useVisibleItems(items, 3)
61+
62+
expect(visibleItems.value).toEqual([1, 2, 3])
63+
64+
expand()
65+
66+
expect(visibleItems.value).toEqual([1, 2, 3, 4, 5, 6])
67+
68+
collapse()
69+
70+
expect(visibleItems.value).toEqual([1, 2, 3])
71+
expect(hasMore.value).toBe(true)
72+
})
73+
})
74+
75+
describe('toggle()', () => {
76+
it('expands on first toggle', () => {
77+
const items = [1, 2, 3, 4, 5]
78+
const { visibleItems, toggle } = useVisibleItems(items, 3)
79+
80+
expect(visibleItems.value).toEqual([1, 2, 3])
81+
82+
toggle()
83+
84+
expect(visibleItems.value).toEqual([1, 2, 3, 4, 5])
85+
})
86+
87+
it('collapses on second toggle', () => {
88+
const items = [1, 2, 3, 4, 5]
89+
const { visibleItems, toggle } = useVisibleItems(items, 3)
90+
91+
expect(visibleItems.value).toEqual([1, 2, 3])
92+
93+
toggle()
94+
95+
expect(visibleItems.value).toEqual([1, 2, 3, 4, 5])
96+
97+
toggle()
98+
99+
expect(visibleItems.value).toEqual([1, 2, 3])
100+
})
101+
})
102+
103+
describe('reactivity', () => {
104+
it('reacts to ref source changes', () => {
105+
const items = ref([1, 2, 3])
106+
const { visibleItems, hasMore, hiddenCount } = useVisibleItems(items, 2)
107+
108+
expect(visibleItems.value).toEqual([1, 2])
109+
expect(hasMore.value).toBe(true)
110+
expect(hiddenCount.value).toBe(1)
111+
112+
items.value = [1, 2]
113+
114+
expect(visibleItems.value).toEqual([1, 2])
115+
expect(hasMore.value).toBe(false)
116+
expect(hiddenCount.value).toBe(0)
117+
})
118+
119+
it('reacts to computed source changes', () => {
120+
const source = ref([1, 2, 3, 4, 5])
121+
const filtered = computed(() => source.value.filter(n => n % 2 === 0))
122+
const { visibleItems, hasMore } = useVisibleItems(filtered, 3)
123+
124+
expect(visibleItems.value).toEqual([2, 4])
125+
expect(hasMore.value).toBe(false)
126+
127+
source.value = [1, 2, 3, 4, 5, 6, 7, 8]
128+
129+
expect(visibleItems.value).toEqual([2, 4, 6])
130+
expect(hasMore.value).toBe(true)
131+
})
132+
133+
it('reacts to getter function source changes', () => {
134+
const count = ref(2)
135+
const { visibleItems } = useVisibleItems(
136+
() => Array.from({ length: count.value }, (_, i) => i),
137+
3,
138+
)
139+
140+
expect(visibleItems.value).toEqual([0, 1])
141+
142+
count.value = 5
143+
144+
expect(visibleItems.value).toEqual([0, 1, 2])
145+
})
146+
})
147+
})

0 commit comments

Comments
 (0)