2222 append-inner-icon =" $dropdown"
2323 @click:clear.stop.prevent =" onInput(null, true)"
2424 @keydown =" onInputKey($event)"
25- @click.stop.prevent =" openMenu()"
25+ @click:control .stop.prevent =" openMenu()"
2626 v-intersect =" onIntersect"
2727 >
2828 <template v-for =" (_ , slot ) of passthroughSlots " v-slot :[slot ]=" scope " >
3535 v-for =" (item, index) in internalModelValue"
3636 :key =" item[modelObjectMeta.keyProp.name]"
3737 class =" v-select__selection"
38+ :class =" {
39+ 'v-select__selection--selected':
40+ index === selectionIndex && effectiveMultiple,
41+ }"
3842 >
3943 <slot
4044 name =" selected-item"
4448 :remove =" () => onInput(item)"
4549 >
4650 <slot name =" item" :item =" item" :search =" search" >
47- <v-chip
48- v-if =" effectiveMultiple"
49- size =" small"
50- :closable =" !!canDeselect && isInteractive"
51- @click:close =" onInput(item)"
52- >
51+ <v-chip v-if =" effectiveMultiple" size =" small" >
52+ <template #append >
53+ <button
54+ v-if =" !!canDeselect && isInteractive"
55+ class =" v-chip__close"
56+ type =" button"
57+ data-testid =" close-chip"
58+ aria-label =" Remove Item"
59+ tabindex =" -1"
60+ @click.stop.prevent =" onInput(item)"
61+ >
62+ <VIcon icon =" $delete" size =" x-small" />
63+ </button >
64+ </template >
5365 {{ itemTitle(item) }}
5466 </v-chip >
5567 <span v-else class =" v-select__selection-text" >
6981 :disabled =" isInteractive"
7082 origin =" top"
7183 location =" bottom"
84+ v-bind =" menuProps"
7285 >
7386 <v-sheet
7487 ref =" menuContentRef"
159172 v-for =" (item, i) in listItems"
160173 :key =" item.key"
161174 @click =" onInput(item.model)"
162- :value =" i "
175+ :value =" item.key "
163176 :class =" {
164177 'pending-selection': pendingSelection === i,
165178 }"
245258 & .c-select--is-menu-active .v-field__append-inner > .v-icon {
246259 transform : rotate (180deg );
247260 }
261+
262+ :has (.v-select__selection--selected ) {
263+ .v-field__input {
264+ caret-color : transparent ;
265+ }
266+ .v-select__selection :not (.v-select__selection--selected ) {
267+ opacity : var (--v-medium-emphasis-opacity );
268+ }
269+ }
248270}
249271
250272.c-select__menu-content {
@@ -402,7 +424,7 @@ import {
402424 ViewModelCollection ,
403425 ModelCollectionNavigationProperty ,
404426} from " coalesce-vue" ;
405- import { VTextField } from " vuetify/components" ;
427+ import { VMenu , VTextField } from " vuetify/components" ;
406428import { Intersect } from " vuetify/directives" ;
407429
408430/* DEV NOTES:
@@ -507,6 +529,10 @@ const props = withDefaults(
507529 rules?: Array < TypedValidationRule< SelectedPkType>> ;
508530
509531 itemTitle?: (item : SelectedModelTypeSingle ) => string | null ;
532+
533+ /** Props to pass to the underlying v-menu component */
534+ menuProps?: VMenu[" $props" ];
535+
510536 create?: {
511537 getLabel: (
512538 search: string,
@@ -595,6 +621,7 @@ const mainValue = ref("");
595621const createItemLoading = ref (false );
596622const createItemError = ref (" " as string | null );
597623const pendingSelection = ref (0 );
624+ const selectionIndex = ref (- 1 );
598625
599626/** The models representing the current selected item(s)
600627 * in the case that only the PK was provided to the component.
@@ -1113,15 +1140,32 @@ function onMenuContentBlur(event: FocusEvent): void {
11131140function onInputKey (event : KeyboardEvent ): void {
11141141 if (! isInteractive .value ) return ;
11151142
1143+ const input = mainInputRef .value ;
1144+ const selectionStart = input? .selectionStart ;
1145+ const value = internalModelValue .value ;
1146+ const length = value .length ;
1147+
11161148 switch (event .key .toLowerCase ()) {
11171149 case " delete" :
11181150 case " backspace" :
11191151 if (! menuOpen .value ) {
11201152 if (effectiveMultiple .value ) {
1121- // Delete only the last item when deleting items with multi-select
1122- const lastItem = internalModelValue .value .at (- 1 );
1123- if (lastItem) {
1124- onInput (lastItem, true );
1153+ if (length == 1 ) {
1154+ onInput (value[0 ], true );
1155+ } else if (selectionIndex .value >= 0 ) {
1156+ // If we have a selection index, remove that specific item
1157+ const itemToRemove = value[selectionIndex .value ];
1158+ if (itemToRemove) {
1159+ onInput (itemToRemove, true );
1160+ // Adjust selection index after removal
1161+ const originalSelectionIndex = selectionIndex .value ;
1162+ selectionIndex .value =
1163+ originalSelectionIndex >= length - 1
1164+ ? length - 2
1165+ : originalSelectionIndex;
1166+ }
1167+ } else if (event .key .toLowerCase () === " backspace" ) {
1168+ selectionIndex .value = length - 1 ;
11251169 }
11261170 } else {
11271171 onInput (null , true );
@@ -1130,11 +1174,57 @@ function onInputKey(event: KeyboardEvent): void {
11301174 event .preventDefault ();
11311175 }
11321176 return ;
1177+ case " arrowleft" :
1178+ case " left" :
1179+ if (! effectiveMultiple .value || menuOpen .value ) return ;
1180+
1181+ if (
1182+ selectionIndex .value < 0 &&
1183+ selectionStart != null &&
1184+ selectionStart > 0
1185+ )
1186+ return ;
1187+
1188+ const prev =
1189+ selectionIndex .value > - 1 ? selectionIndex .value - 1 : length - 1 ;
1190+
1191+ if (internalModelValue .value [prev]) {
1192+ selectionIndex .value = prev;
1193+ } else {
1194+ const searchLength = search .value ? .length ?? 0 ;
1195+ selectionIndex .value = - 1 ;
1196+ input? .setSelectionRange (searchLength, searchLength);
1197+ }
1198+ event .stopPropagation ();
1199+ event .preventDefault ();
1200+ return ;
1201+ case " arrowright" :
1202+ case " right" :
1203+ if (! effectiveMultiple .value || menuOpen .value ) return ;
1204+
1205+ if (selectionIndex .value < 0 ) return ;
1206+
1207+ const next = selectionIndex .value + 1 ;
1208+
1209+ if (internalModelValue .value [next]) {
1210+ selectionIndex .value = next;
1211+ } else {
1212+ selectionIndex .value = - 1 ;
1213+ input? .setSelectionRange (0 , 0 );
1214+ }
1215+ event .stopPropagation ();
1216+ event .preventDefault ();
1217+ return ;
11331218 case " esc" :
11341219 case " escape" :
1220+ if (! menuOpen .value && selectionIndex .value >= 0 ) {
1221+ selectionIndex .value = - 1 ;
1222+ mainInputRef .value ? .focus ();
1223+ } else {
1224+ closeMenu (true );
1225+ }
11351226 event .stopPropagation ();
11361227 event .preventDefault ();
1137- closeMenu (true );
11381228 return ;
11391229 case " " :
11401230 case " enter" :
@@ -1208,7 +1298,7 @@ function onIntersect(isIntersecting: boolean) {
12081298 // Doesn't work reliably without a small delay
12091299 setTimeout (() => {
12101300 mainInputRef .value ? .focus ();
1211- }, 10 );
1301+ }, 50 );
12121302}
12131303
12141304async function openMenu (select ?: boolean ): Promise<void> {
@@ -1223,6 +1313,7 @@ async function openMenu(select?: boolean): Promise<void> {
12231313
12241314 if (menuOpen .value ) return ;
12251315 menuOpen .value = true ;
1316+ selectionIndex .value = - 1 ; // Reset selection index when menu opens
12261317
12271318 if (props .reloadOnOpen ) listCaller ();
12281319
@@ -1267,6 +1358,7 @@ function closeMenu(force = false): void {
12671358
12681359 menuOpenForced .value = false ;
12691360 menuOpen .value = false ;
1361+ selectionIndex .value = - 1 ;
12701362 mainInputRef .value ? .focus ();
12711363}
12721364
0 commit comments