feat(laptops): implement keyboard arrow navigation and select-on-enter for employee assignment dropdown

This commit is contained in:
NAS-Admin 2026-06-01 09:33:31 +09:00
parent 5e46514f62
commit ab27bfc34d

View file

@ -434,8 +434,13 @@
</label> </label>
<div class="relative"> <div class="relative">
<input type="text" x-model="userSearchQuery" <input type="text" x-model="userSearchQuery"
@focus="showUserDropdown = true; userSearchQuery = ''" @focus="showUserDropdown = true; userSearchQuery = ''; userHighlightIndex = -1"
@input="showUserDropdown = true" placeholder="사용자 검색 (이름, 사번)" @input="showUserDropdown = true; userHighlightIndex = -1"
@keydown.arrow-down.prevent="if(showUserDropdown) { userHighlightIndex = (userHighlightIndex + 1) % filteredUsers.length; adjustScroll(); }"
@keydown.arrow-up.prevent="if(showUserDropdown) { userHighlightIndex = (userHighlightIndex - 1 + filteredUsers.length) % filteredUsers.length; adjustScroll(); }"
@keydown.enter.prevent="if(showUserDropdown && userHighlightIndex >= 0 && filteredUsers[userHighlightIndex]) { selectUser(filteredUsers[userHighlightIndex]); } else if (showUserDropdown) { showUserDropdown = false; } else { submitAsset(); }"
@keydown.space="if(showUserDropdown && userHighlightIndex >= 0 && filteredUsers[userHighlightIndex]) { $event.preventDefault(); selectUser(filteredUsers[userHighlightIndex]); }"
placeholder="사용자 검색 (이름, 사번)"
:disabled="formData.status !== 'assigned'" :disabled="formData.status !== 'assigned'"
:class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'" :class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'"
class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700"> class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
@ -458,16 +463,19 @@
<!-- User Search Dropdown --> <!-- User Search Dropdown -->
<div x-show="showUserDropdown" x-transition <div x-show="showUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar"> class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
<template x-for="user in filteredUsers" :key="user.id"> <template x-for="(user, index) in filteredUsers" :key="user.id">
<div @click="selectUser(user)" <div @click="selectUser(user)"
:class="userHighlightIndex === index ? 'bg-blue-50' : ''"
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group"> class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div> <div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600" <div class="text-sm font-bold text-slate-700 transition-colors"
:class="userHighlightIndex === index ? 'text-blue-600' : 'group-hover:text-blue-600'"
x-text="user.name"></div> x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium" <div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div> x-text="user.dept_name || '부서 정보 없음'"></div>
</div> </div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400" <div class="text-[10px] font-black text-slate-300 transition-colors"
:class="userHighlightIndex === index ? 'text-blue-400' : 'group-hover:text-blue-400'"
x-text="user.emp_id"></div> x-text="user.emp_id"></div>
</div> </div>
</template> </template>
@ -608,6 +616,7 @@
disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: '' disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: ''
}, },
userSearchQuery: '', userSearchQuery: '',
userHighlightIndex: -1,
showUserDropdown: false, showUserDropdown: false,
disposalSearchQuery: '', disposalSearchQuery: '',
showDisposalDropdown: false, showDisposalDropdown: false,
@ -660,6 +669,7 @@
this.formData.current_user_id = user.id; this.formData.current_user_id = user.id;
this.userSearchQuery = `${user.name} (${user.emp_id})`; this.userSearchQuery = `${user.name} (${user.emp_id})`;
this.showUserDropdown = false; this.showUserDropdown = false;
this.userHighlightIndex = -1;
this.updateAssignedUserName(); this.updateAssignedUserName();
// Do not auto-change status to assigned here because user might be in stock mode // Do not auto-change status to assigned here because user might be in stock mode
}, },
@ -667,12 +677,34 @@
clearUser() { clearUser() {
this.formData.current_user_id = ''; this.formData.current_user_id = '';
this.userSearchQuery = ''; this.userSearchQuery = '';
this.userHighlightIndex = -1;
this.updateAssignedUserName(); this.updateAssignedUserName();
if (this.formData.status === 'assigned') { if (this.formData.status === 'assigned') {
this.formData.status = 'stock'; this.formData.status = 'stock';
} }
}, },
adjustScroll() {
this.$nextTick(() => {
const container = this.$el.querySelector('.max-h-60');
if (!container) return;
const items = container.querySelectorAll('.cursor-pointer');
const activeItem = items[this.userHighlightIndex];
if (!activeItem) return;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
const elemTop = activeItem.offsetTop;
const elemBottom = elemTop + activeItem.offsetHeight;
if (elemTop < containerTop) {
container.scrollTop = elemTop;
} else if (elemBottom > containerBottom) {
container.scrollTop = elemBottom - container.clientHeight;
}
});
},
get isAssetTagDuplicate() { get isAssetTagDuplicate() {
const tag = (this.formData.asset_tag || '').trim(); const tag = (this.formData.asset_tag || '').trim();
if (!tag) return false; if (!tag) return false;