-
-
Notifications
You must be signed in to change notification settings - Fork 424
Expand file tree
/
Copy pathColumnPicker.vue
More file actions
166 lines (149 loc) · 5.18 KB
/
ColumnPicker.vue
File metadata and controls
166 lines (149 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
<script setup lang="ts">
import type { ColumnConfig, ColumnId } from '#shared/types/preferences'
const props = defineProps<{
columns: ColumnConfig[]
}>()
const emit = defineEmits<{
toggle: [columnId: ColumnId]
reset: []
}>()
const isOpen = ref(false)
const buttonRef = useTemplateRef('buttonRef')
const menuRef = useTemplateRef('menuRef')
const menuId = useId()
// Close on click outside (check both button and menu)
function handleClickOutside(event: MouseEvent) {
const target = event.target as Node
const isOutsideButton = buttonRef.value && !buttonRef.value.contains(target)
const isOutsideMenu = !menuRef.value || !menuRef.value.contains(target)
if (isOutsideButton && isOutsideMenu) {
isOpen.value = false
}
}
// Close on Escape key
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && isOpen.value) {
isOpen.value = false
buttonRef.value?.focus()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
document.removeEventListener('keydown', handleKeydown)
})
// Columns that can be toggled (name is always visible)
const toggleableColumns = computed(() => props.columns.filter(col => col.id !== 'name'))
// Map column IDs to i18n keys
const columnLabelKey: Record<string, string> = {
name: 'filters.columns.name',
version: 'filters.columns.version',
description: 'filters.columns.description',
downloads: 'filters.columns.downloads',
updated: 'filters.columns.updated',
maintainers: 'filters.columns.maintainers',
keywords: 'filters.columns.keywords',
qualityScore: 'filters.columns.quality_score',
popularityScore: 'filters.columns.popularity_score',
maintenanceScore: 'filters.columns.maintenance_score',
combinedScore: 'filters.columns.combined_score',
security: 'filters.columns.security',
}
function getColumnLabel(id: string): string {
const key = columnLabelKey[id]
return key ? $t(key) : id
}
function handleReset() {
emit('reset')
isOpen.value = false
}
</script>
<template>
<div class="relative">
<button
ref="buttonRef"
type="button"
class="btn-ghost inline-flex items-center gap-1.5 px-3 py-1.5 border border-border rounded-md hover:border-border-hover focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
:aria-expanded="isOpen"
aria-haspopup="true"
:aria-controls="menuId"
@click.stop="isOpen = !isOpen"
>
<span class="i-carbon-column w-4 h-4" aria-hidden="true" />
<span class="font-mono text-sm">{{ $t('filters.columns.title') }}</span>
</button>
<Transition name="dropdown">
<div
v-if="isOpen"
ref="menuRef"
:id="menuId"
class="absolute right-0 mt-2 w-56 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
role="group"
:aria-label="$t('filters.columns.show')"
>
<div class="py-1">
<div
class="px-3 py-2 text-xs font-mono text-fg-subtle uppercase tracking-wider border-b border-border"
aria-hidden="true"
>
{{ $t('filters.columns.show') }}
</div>
<div class="py-1 max-h-64 overflow-y-auto">
<label
v-for="column in toggleableColumns"
:key="column.id"
class="flex items-center px-3 py-2 transition-colors duration-200"
:class="
column.disabled
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-bg-muted cursor-pointer'
"
>
<input
type="checkbox"
:checked="column.visible"
:disabled="column.disabled"
:aria-describedby="column.disabled ? `${column.id}-disabled-reason` : undefined"
class="w-4 h-4 accent-fg bg-bg-muted border-border rounded disabled:opacity-50"
@change="!column.disabled && emit('toggle', column.id)"
/>
<span class="ml-2 text-sm text-fg-muted font-mono flex-1">
{{ getColumnLabel(column.id) }}
</span>
<span
v-if="column.disabled"
:id="`${column.id}-disabled-reason`"
class="text-xs text-fg-subtle italic"
>
{{ $t('filters.columns.coming_soon') }}
</span>
</label>
</div>
<div class="border-t border-border py-1">
<button
type="button"
class="w-full px-3 py-2 text-left text-sm font-mono text-fg-muted hover:bg-bg-muted hover:text-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset"
@click="handleReset"
>
{{ $t('filters.columns.reset') }}
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style>