mirror of
https://github.com/nocodb/nocodb.git
synced 2026-06-01 21:32:04 +00:00
Merge pull request #13927 from nocodb/nc-fix/link-modal-arrow-keys
Nc fix(nc-gui): arrow-key navigation in link/unlink record modals
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,29 +349,8 @@ watch([filterQueryRef, isDataExist], () => {
|
||||
}
|
||||
})
|
||||
|
||||
const linkedShortcuts = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
vModel.value = false
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
try {
|
||||
e.target?.nextElementSibling?.focus()
|
||||
} catch (e) {}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
try {
|
||||
e.target?.previousElementSibling?.focus()
|
||||
} catch (e) {}
|
||||
} else 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)
|
||||
@@ -381,8 +362,6 @@ onMounted(() => {
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const scrollContainerRef = ref<HTMLElement>()
|
||||
|
||||
const ROW_VIRTUAL_MARGIN = 5
|
||||
|
||||
const rowSlice = reactive({ start: 0, end: 0 })
|
||||
@@ -445,7 +424,6 @@ onUnmounted(() => {
|
||||
resetChildrenListOffsetCount()
|
||||
resetChildrenCache()
|
||||
childrenListPagination.query = ''
|
||||
window.removeEventListener('keydown', linkedShortcuts)
|
||||
})
|
||||
|
||||
const onFilterChange = () => {
|
||||
@@ -457,18 +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')
|
||||
}
|
||||
}
|
||||
}
|
||||
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,28 +355,6 @@ const onDeletedRecord = async () => {
|
||||
loadChildrenExcludedList(rowState.value, true)
|
||||
}
|
||||
|
||||
const linkedShortcuts = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
vModel.value = false
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
try {
|
||||
e.target?.nextElementSibling?.focus()
|
||||
} catch (e) {}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
try {
|
||||
e.target?.previousElementSibling?.focus()
|
||||
} catch (e) {}
|
||||
} else 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 })
|
||||
@@ -435,7 +415,6 @@ const visibleRows = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', linkedShortcuts)
|
||||
loadRelatedTableMeta()
|
||||
|
||||
// Load initial chunk
|
||||
@@ -452,7 +431,6 @@ onUnmounted(() => {
|
||||
resetChildrenExcludedOffsetCount()
|
||||
resetExcludedCache()
|
||||
childrenExcludedListPagination.query = ''
|
||||
window.removeEventListener('keydown', linkedShortcuts)
|
||||
})
|
||||
|
||||
const onFilterChange = () => {
|
||||
@@ -464,20 +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')
|
||||
}
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
@@ -880,12 +880,14 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
|
||||
isChildrenExcludedListLinked.value[index] = true
|
||||
isChildrenListLinked.value[index] = true
|
||||
excludedLinkedState.value.set(index, true)
|
||||
childrenCachedLinkedState.value.set(index, true)
|
||||
return
|
||||
}
|
||||
try {
|
||||
isChildrenExcludedListLoading.value[index] = true
|
||||
isChildrenListLoading.value[index] = true
|
||||
excludedLoadingState.value.set(index, true)
|
||||
childrenCachedLoadingState.value.set(index, true)
|
||||
|
||||
childrenListOffsetCount.value = childrenListOffsetCount.value + 1
|
||||
childrenExcludedOffsetCount.value = childrenExcludedOffsetCount.value + 1
|
||||
@@ -904,6 +906,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
|
||||
isChildrenExcludedListLinked.value[index] = true
|
||||
isChildrenListLinked.value[index] = true
|
||||
excludedLinkedState.value.set(index, true)
|
||||
childrenCachedLinkedState.value.set(index, true)
|
||||
|
||||
if (!isSingleTargetRelation.value) {
|
||||
childrenListCount.value = childrenListCount.value + 1
|
||||
@@ -929,6 +932,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
|
||||
isChildrenExcludedListLoading.value[index] = false
|
||||
isChildrenListLoading.value[index] = false
|
||||
excludedLoadingState.value.set(index, false)
|
||||
childrenCachedLoadingState.value.set(index, false)
|
||||
}
|
||||
|
||||
_reloadData?.({ shouldShowLoading: false, path: path.value })
|
||||
|
||||
Reference in New Issue
Block a user