1+ <script setup lang="ts">
2+ import { computed , onBeforeUnmount , onMounted , ref } from ' vue' ;
3+ import { DownOutlined , SortAscendingOutlined , SortDescendingOutlined } from ' @ant-design/icons-vue' ;
4+ import type { IconResourceSortMode } from ' ./utils/state' ;
5+
6+ type SortGroup = ' name' | ' added' ;
7+
8+ interface SortingText {
9+ sort: string ;
10+ sortOptions: {
11+ added: string ;
12+ ascending: string ;
13+ descending: string ;
14+ name: string ;
15+ };
16+ }
17+
18+ const props = defineProps <{
19+ modelValue: IconResourceSortMode ;
20+ text: SortingText ;
21+ }>();
22+
23+ const emit = defineEmits <{
24+ ' update:modelValue' : [mode : IconResourceSortMode ];
25+ }>();
26+
27+ const sortGroups: SortGroup [] = [' name' , ' added' ];
28+ const sortRoot = ref <HTMLElement >();
29+ const menuOpen = ref (false );
30+ const activeGroup = computed (() => props .modelValue .startsWith (' name' ) ? ' name' : ' added' );
31+ const descending = computed (() => props .modelValue .endsWith (' Desc' ));
32+ const resolveSortMode = (group : SortGroup , useDescending : boolean ) => group === ' name'
33+ ? useDescending ? ' nameDesc' : ' nameAsc'
34+ : useDescending ? ' addedDesc' : ' addedAsc' ;
35+ const toggleDirection = () => {
36+ emit (' update:modelValue' , resolveSortMode (activeGroup .value , ! descending .value ));
37+ menuOpen .value = false ;
38+ };
39+ const selectSortGroup = (group : SortGroup ) => {
40+ emit (' update:modelValue' , resolveSortMode (group , descending .value ));
41+ menuOpen .value = false ;
42+ };
43+ const closeMenuOnDocumentClick = (event : MouseEvent ) => {
44+ if (! sortRoot .value ?.contains (event .target as Node )) menuOpen .value = false ;
45+ };
46+ const closeMenuOnEscape = (event : KeyboardEvent ) => {
47+ if (event .key === ' Escape' ) menuOpen .value = false ;
48+ };
49+
50+ onMounted (() => {
51+ document .addEventListener (' click' , closeMenuOnDocumentClick );
52+ document .addEventListener (' keydown' , closeMenuOnEscape );
53+ });
54+ onBeforeUnmount (() => {
55+ document .removeEventListener (' click' , closeMenuOnDocumentClick );
56+ document .removeEventListener (' keydown' , closeMenuOnEscape );
57+ });
58+ </script >
59+
60+ <template >
61+ <div ref =" sortRoot" class =" sort-control" >
62+ <button type =" button" class =" sort-trigger sort-direction-trigger"
63+ :title =" `${text.sort}: ${text.sortOptions[descending ? 'descending' : 'ascending']}`"
64+ :aria-label =" `${text.sort}: ${text.sortOptions[descending ? 'descending' : 'ascending']}`"
65+ @click =" toggleDirection" >
66+ <SortDescendingOutlined v-if =" descending " aria-hidden="true" />
67+ <SortAscendingOutlined v-else aria-hidden="true" />
68+ </button >
69+ <button type =" button" class =" sort-trigger sort-field-trigger" :class =" { active: menuOpen }" aria-haspopup =" menu"
70+ :aria-expanded =" menuOpen" :title =" `${text.sort}: ${text.sortOptions[activeGroup]}`"
71+ :aria-label =" `${text.sort}: ${text.sortOptions[activeGroup]}`" @click =" menuOpen = !menuOpen" >
72+ <DownOutlined aria-hidden="true" />
73+ </button >
74+ <Transition name="sort-menu">
75+ <div v-if =" menuOpen" class =" sort-menu" role =" menu" >
76+ <button v-for =" group in sortGroups" :key =" group" type =" button" role =" menuitemradio"
77+ :aria-checked =" activeGroup === group" :class =" { active: activeGroup === group }"
78+ @click =" selectSortGroup(group)" >
79+ {{ text.sortOptions[group] }}
80+ </button >
81+ </div >
82+ </Transition >
83+ </div >
84+ </template >
85+
86+ <style scoped lang="scss">
87+ .sort-control {
88+ position : relative ;
89+ display : flex ;
90+ flex : 0 0 auto ;
91+ }
92+
93+ .sort-trigger {
94+ display : grid ;
95+ height : 38px ;
96+ margin : 0 ;
97+ padding : 0 ;
98+ border : 1px solid var (--vp-c-divider );
99+ place-items : center ;
100+ background : var (--vp-c-bg-soft );
101+ color : var (--vp-c-text-2 );
102+ cursor : pointer ;
103+
104+ :deep (.anticon ) {
105+ font-size : 18px ;
106+ }
107+
108+ & :hover ,
109+ & .active {
110+ position : relative ;
111+ z-index : 1 ;
112+ border-color : var (--vp-c-brand-1 );
113+ color : var (--vp-c-brand-1 );
114+ }
115+
116+ & .active {
117+ background : var (--vp-c-brand-soft );
118+ }
119+ }
120+
121+ .sort-direction-trigger {
122+ width : 38px ;
123+ border-radius : 9px 0 0 9px ;
124+ }
125+
126+ .sort-field-trigger {
127+ width : 30px ;
128+ margin-left : -1px ;
129+ border-radius : 0 9px 9px 0 ;
130+
131+ :deep (.anticon ) {
132+ font-size : 12px ;
133+ transition : transform 0.15s ease ;
134+ }
135+
136+ & [aria-expanded = ' true' ] :deep(.anticon ) {
137+ transform : rotate (180deg );
138+ }
139+ }
140+
141+ .sort-menu {
142+ position : absolute ;
143+ z-index : 20 ;
144+ top : calc (100% + 8px );
145+ right : 0 ;
146+ display : grid ;
147+ min-width : 130px ;
148+ padding : 8px ;
149+ border : 1px solid var (--vp-c-divider );
150+ border-radius : 10px ;
151+ background : var (--vp-c-bg-elv );
152+ box-shadow : var (--vp-shadow-3 );
153+
154+ button {
155+ width : 100% ;
156+ padding : 7px 10px ;
157+ border : 0 ;
158+ border-radius : 6px ;
159+ background : transparent ;
160+ color : var (--vp-c-text-2 );
161+ font : inherit ;
162+ text-align : left ;
163+ cursor : pointer ;
164+
165+ & :hover {
166+ background : var (--vp-c-bg-soft );
167+ color : var (--vp-c-text-1 );
168+ }
169+
170+ & .active {
171+ background : var (--vp-c-brand-soft );
172+ color : var (--vp-c-brand-1 );
173+ }
174+ }
175+ }
176+
177+ .sort-menu-enter-active ,
178+ .sort-menu-leave-active {
179+ transition : opacity 0.15s ease , transform 0.15s ease ;
180+ }
181+
182+ .sort-menu-enter-from ,
183+ .sort-menu-leave-to {
184+ opacity : 0 ;
185+ transform : translateY (-6px );
186+ }
187+ </style >
0 commit comments