mirror of
https://github.com/nocodb/nocodb.git
synced 2026-06-01 23:02:12 +00:00
refactor(nc-gui): extract LTAR list key-nav into shared composable
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
95
packages/nc-gui/composables/useLTARListKeyNav.ts
Normal file
95
packages/nc-gui/composables/useLTARListKeyNav.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user