refactor(nc-gui): extract LTAR list key-nav into shared composable

This commit is contained in:
Ramesh Mane
2026-05-28 13:10:59 +00:00
parent a59b9ca1da
commit 72c492f3bc
3 changed files with 128 additions and 127 deletions

View File

@@ -48,6 +48,8 @@ const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const filterQueryRef = ref<HTMLInputElement>()
const scrollContainerRef = ref<HTMLElement>()
const { isDataReadOnly } = useRoles()
const { isSharedBase } = storeToRefs(useBase())
@@ -347,51 +349,8 @@ watch([filterQueryRef, isDataExist], () => {
}
})
function focusListItemByIndex(idx: number) {
const items = scrollContainerRef.value
? Array.from(scrollContainerRef.value.querySelectorAll<HTMLElement>('[data-testid="nc-child-list-item"]'))
: []
const wrapper = items[idx]
const focusable = wrapper?.querySelector<HTMLElement>('[tabindex="0"]') ?? wrapper
focusable?.focus()
}
function linkedShortcuts(e: KeyboardEvent) {
if (e.key === 'Escape') {
vModel.value = false
return
}
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const target = e.target as HTMLElement | null
const currentWrapper = target?.closest<HTMLElement>('[data-testid="nc-child-list-item"]')
if (!currentWrapper || !scrollContainerRef.value) return
e.preventDefault()
const items = Array.from(scrollContainerRef.value.querySelectorAll<HTMLElement>('[data-testid="nc-child-list-item"]'))
const idx = items.indexOf(currentWrapper)
if (idx === -1) return
if (e.key === 'ArrowDown') {
if (idx < items.length - 1) focusListItemByIndex(idx + 1)
} else if (e.key === 'ArrowUp') {
if (idx === 0) filterQueryRef.value?.focus()
else focusListItemByIndex(idx - 1)
}
return
}
if (!expandedFormDlg.value && e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch (e) {}
}
}
onMounted(() => {
loadRelatedTableMeta()
window.addEventListener('keydown', linkedShortcuts)
// Load initial chunk for virtual scroll
fetchChildrenChunk(0)
@@ -403,8 +362,6 @@ onMounted(() => {
}, 100)
})
const scrollContainerRef = ref<HTMLElement>()
const ROW_VIRTUAL_MARGIN = 5
const rowSlice = reactive({ start: 0, end: 0 })
@@ -467,7 +424,6 @@ onUnmounted(() => {
resetChildrenListOffsetCount()
resetChildrenCache()
childrenListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts)
})
const onFilterChange = () => {
@@ -479,23 +435,21 @@ const onFilterChange = () => {
const isSearchInputFocused = ref(false)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (!childrenListPagination.query) emit('escape')
filterQueryRef.value?.blur()
} else if (e.key === 'Enter') {
const { handleSearchKeydown: handleKeyDown } = useLTARListKeyNav({
scrollContainerRef,
filterQueryRef,
itemTestId: 'nc-child-list-item',
expandedFormDlg,
closeModal: () => {
vModel.value = false
},
getQuery: () => childrenListPagination.query,
onEscapeEmptyQuery: () => emit('escape'),
onEnterWithQuery: () => {
const list = childrenList.value?.list ?? state.value?.[colTitle.value]
if (childrenListPagination.query && ncIsArray(list) && list.length) {
linkOrUnLink(list[0], '0')
}
} else if (e.key === 'ArrowDown') {
e.preventDefault()
focusListItemByIndex(0)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
}
}
if (ncIsArray(list) && list.length) linkOrUnLink(list[0], '0')
},
})
</script>
<template>

View File

@@ -32,6 +32,8 @@ const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref<HTMLInputElement>()
const scrollContainerRef = ref<HTMLElement>()
const { t } = useI18n()
const { $e } = useNuxtApp()
@@ -353,50 +355,6 @@ const onDeletedRecord = async () => {
loadChildrenExcludedList(rowState.value, true)
}
function focusListItemByIndex(idx: number) {
const items = scrollContainerRef.value
? Array.from(scrollContainerRef.value.querySelectorAll<HTMLElement>('[data-testid="nc-excluded-list-item"]'))
: []
const wrapper = items[idx]
const focusable = wrapper?.querySelector<HTMLElement>('[tabindex="0"]') ?? wrapper
focusable?.focus()
}
function linkedShortcuts(e: KeyboardEvent) {
if (e.key === 'Escape') {
vModel.value = false
return
}
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const target = e.target as HTMLElement | null
const currentWrapper = target?.closest<HTMLElement>('[data-testid="nc-excluded-list-item"]')
if (!currentWrapper || !scrollContainerRef.value) return
e.preventDefault()
const items = Array.from(scrollContainerRef.value.querySelectorAll<HTMLElement>('[data-testid="nc-excluded-list-item"]'))
const idx = items.indexOf(currentWrapper)
if (idx === -1) return
if (e.key === 'ArrowDown') {
if (idx < items.length - 1) focusListItemByIndex(idx + 1)
} else if (e.key === 'ArrowUp') {
if (idx === 0) filterQueryRef.value?.focus()
else focusListItemByIndex(idx - 1)
}
return
}
if (!expandedFormDlg.value && e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch (e) {}
}
}
const scrollContainerRef = ref<HTMLElement>()
const ROW_VIRTUAL_MARGIN = 5
const rowSlice = reactive({ start: 0, end: 0 })
@@ -457,7 +415,6 @@ const visibleRows = computed(() => {
})
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
loadRelatedTableMeta()
// Load initial chunk
@@ -474,7 +431,6 @@ onUnmounted(() => {
resetChildrenExcludedOffsetCount()
resetExcludedCache()
childrenExcludedListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts)
})
const onFilterChange = () => {
@@ -486,25 +442,21 @@ const onFilterChange = () => {
const isSearchInputFocused = ref(false)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (!childrenExcludedListPagination.query) emit('escape')
filterQueryRef.value?.blur()
} else if (e.key === 'Enter') {
if (
childrenExcludedListPagination.query &&
ncIsArray(childrenExcludedList.value?.list) &&
childrenExcludedList.value?.list.length
) {
onClick(childrenExcludedList.value?.list[0], '0')
}
} else if (e.key === 'ArrowDown') {
e.preventDefault()
focusListItemByIndex(0)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
}
}
const { handleSearchKeydown: handleKeyDown } = useLTARListKeyNav({
scrollContainerRef,
filterQueryRef,
itemTestId: 'nc-excluded-list-item',
expandedFormDlg,
closeModal: () => {
vModel.value = false
},
getQuery: () => childrenExcludedListPagination.query,
onEscapeEmptyQuery: () => emit('escape'),
onEnterWithQuery: () => {
const list = childrenExcludedList.value?.list
if (ncIsArray(list) && list.length) onClick(list[0], '0')
},
})
</script>
<template>

View File

@@ -0,0 +1,95 @@
import type { Ref } from 'vue'
interface UseLTARListKeyNavOptions {
scrollContainerRef: Ref<HTMLElement | undefined>
filterQueryRef: Ref<HTMLInputElement | undefined>
itemTestId: string
expandedFormDlg: Ref<boolean>
closeModal: () => void
getQuery: () => string
onEscapeEmptyQuery: () => void
onEnterWithQuery: () => void
}
export function useLTARListKeyNav(options: UseLTARListKeyNavOptions) {
const {
scrollContainerRef,
filterQueryRef,
itemTestId,
expandedFormDlg,
closeModal,
getQuery,
onEscapeEmptyQuery,
onEnterWithQuery,
} = options
function getItems(): HTMLElement[] {
const container = scrollContainerRef.value
if (!container) return []
return Array.from(container.querySelectorAll<HTMLElement>(`[data-testid="${itemTestId}"]`))
}
function focusListItemByIndex(idx: number) {
const wrapper = getItems()[idx]
const focusable = wrapper?.querySelector<HTMLElement>('[tabindex="0"]') ?? wrapper
focusable?.focus()
}
function onWindowKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
closeModal()
return
}
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const target = e.target as HTMLElement | null
const currentWrapper = target?.closest<HTMLElement>(`[data-testid="${itemTestId}"]`)
if (!currentWrapper || !scrollContainerRef.value) return
e.preventDefault()
const items = getItems()
const idx = items.indexOf(currentWrapper)
if (idx === -1) return
if (e.key === 'ArrowDown') {
if (idx < items.length - 1) focusListItemByIndex(idx + 1)
} else if (idx === 0) {
filterQueryRef.value?.focus()
} else {
focusListItemByIndex(idx - 1)
}
return
}
if (!expandedFormDlg.value && e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch {}
}
}
function handleSearchKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (!getQuery()) onEscapeEmptyQuery()
filterQueryRef.value?.blur()
} else if (e.key === 'Enter') {
if (getQuery()) onEnterWithQuery()
} else if (e.key === 'ArrowDown') {
e.preventDefault()
focusListItemByIndex(0)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keydown', onWindowKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onWindowKeydown)
})
return { handleSearchKeydown }
}