修改树的显示
This commit is contained in:
parent
bbcb734f72
commit
d4b1e41171
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
|
|
@ -624,8 +624,15 @@ watch(mockTreeData, (newData) => {
|
|||
}, { deep: true, immediate: true })
|
||||
|
||||
const selectedId = ref<string | null>('1')
|
||||
const focusedId = ref<string | null>('1')
|
||||
const expandedIds = ref<Set<string>>(new Set())
|
||||
const dialogExpandedIds = ref<Set<string>>(new Set())
|
||||
const treeRootRef = ref<HTMLElement | null>(null)
|
||||
const contextMenuRef = ref<HTMLElement | null>(null)
|
||||
const addNodeDialogRef = ref<HTMLElement | null>(null)
|
||||
const operationDialogRef = ref<HTMLElement | null>(null)
|
||||
const liveMessage = ref('')
|
||||
const contextMenuTriggerId = ref<string | null>(null)
|
||||
|
||||
const contextMenu = ref({
|
||||
show: false,
|
||||
|
|
@ -818,8 +825,17 @@ const handleToggle = (id: string, isDialog = false) => {
|
|||
const targetSet = isDialog ? dialogExpandedIds : expandedIds
|
||||
if (targetSet.value.has(id)) {
|
||||
targetSet.value.delete(id)
|
||||
if (!isDialog && focusedId.value && focusedId.value !== id && isDescendantId(id, focusedId.value)) {
|
||||
setActiveNode(id, true, true)
|
||||
}
|
||||
if (!isDialog) {
|
||||
announce('Node collapsed')
|
||||
}
|
||||
} else {
|
||||
targetSet.value.add(id)
|
||||
if (!isDialog) {
|
||||
announce('Node expanded')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -830,6 +846,7 @@ const handleDoubleClick = (id: string, node: TreeNode) => {
|
|||
}
|
||||
|
||||
const handleSelect = (id: string, node: TreeNode) => {
|
||||
focusedId.value = id
|
||||
selectedId.value = id
|
||||
emit('update:modelValue', id)
|
||||
emit('select', node)
|
||||
|
|
@ -846,11 +863,13 @@ const expandAll = () => {
|
|||
})
|
||||
}
|
||||
expandNode(mockTreeData.value)
|
||||
announce('All nodes expanded')
|
||||
}
|
||||
|
||||
// 折叠所有节点
|
||||
const collapseAll = () => {
|
||||
expandedIds.value.clear()
|
||||
announce('All nodes collapsed')
|
||||
}
|
||||
|
||||
const isExpanded = (id: string, isDialog = false) => {
|
||||
|
|
@ -858,6 +877,202 @@ const isExpanded = (id: string, isDialog = false) => {
|
|||
return targetSet.value.has(id)
|
||||
}
|
||||
const isSelected = (id: string) => selectedId.value === id
|
||||
const getNodeById = (id: string) => findNode(mockTreeData.value, id)
|
||||
const getVisibleNodeIndex = (id: string) => flattenedNodes.value.findIndex(item => item.node.id === id)
|
||||
const getNodeElement = (id: string): HTMLElement | null => {
|
||||
if (!treeRootRef.value) return null
|
||||
const safeId = id.replace(/"/g, '\\"')
|
||||
return treeRootRef.value.querySelector(`[data-node-id="${safeId}"]`) as HTMLElement | null
|
||||
}
|
||||
const focusNodeElement = (id: string) => {
|
||||
nextTick(() => {
|
||||
getNodeElement(id)?.focus()
|
||||
})
|
||||
}
|
||||
const setActiveNode = (id: string, select = true, focus = false) => {
|
||||
const node = getNodeById(id)
|
||||
if (!node) return
|
||||
focusedId.value = id
|
||||
if (select) {
|
||||
handleSelect(id, node)
|
||||
}
|
||||
if (focus) {
|
||||
focusNodeElement(id)
|
||||
}
|
||||
}
|
||||
const isTabStop = (id: string, index: number) => {
|
||||
if (focusedId.value) return focusedId.value === id
|
||||
return index === 0
|
||||
}
|
||||
const moveFocusByOffset = (offset: number) => {
|
||||
if (flattenedNodes.value.length === 0) return
|
||||
const currentId = focusedId.value || selectedId.value || flattenedNodes.value[0].node.id
|
||||
const currentIndex = getVisibleNodeIndex(currentId)
|
||||
const fallbackIndex = currentIndex < 0 ? 0 : currentIndex
|
||||
const nextIndex = Math.min(flattenedNodes.value.length - 1, Math.max(0, fallbackIndex + offset))
|
||||
const nextNode = flattenedNodes.value[nextIndex]?.node
|
||||
if (nextNode) {
|
||||
setActiveNode(nextNode.id, true, true)
|
||||
}
|
||||
}
|
||||
const focusFirstVisibleNode = () => {
|
||||
const first = flattenedNodes.value[0]?.node
|
||||
if (first) setActiveNode(first.id, true, true)
|
||||
}
|
||||
const focusLastVisibleNode = () => {
|
||||
const last = flattenedNodes.value[flattenedNodes.value.length - 1]?.node
|
||||
if (last) setActiveNode(last.id, true, true)
|
||||
}
|
||||
const isDescendantId = (ancestorId: string, nodeId: string) => {
|
||||
if (ancestorId === nodeId) return true
|
||||
let currentParent = findParentNode(mockTreeData.value, nodeId)
|
||||
while (currentParent) {
|
||||
if (currentParent.id === ancestorId) return true
|
||||
currentParent = findParentNode(mockTreeData.value, currentParent.id)
|
||||
}
|
||||
return false
|
||||
}
|
||||
const announce = (message: string) => {
|
||||
liveMessage.value = ''
|
||||
nextTick(() => {
|
||||
liveMessage.value = message
|
||||
})
|
||||
}
|
||||
const openContextMenuFromKeyboard = (nodeId: string) => {
|
||||
const targetEl = getNodeElement(nodeId)
|
||||
const rect = targetEl?.getBoundingClientRect()
|
||||
const x = rect ? Math.min(rect.left + 24, window.innerWidth - 200) : 24
|
||||
const y = rect ? Math.min(rect.bottom, window.innerHeight - 320) : 24
|
||||
contextMenuTriggerId.value = nodeId
|
||||
contextMenu.value = {
|
||||
show: true,
|
||||
x,
|
||||
y,
|
||||
nodeId
|
||||
}
|
||||
}
|
||||
const focusFirstMenuItem = () => {
|
||||
const item = contextMenuRef.value?.querySelector('.menu-item[tabindex="0"]') as HTMLElement | null
|
||||
item?.focus()
|
||||
}
|
||||
const focusNodeAfterContextMenu = () => {
|
||||
if (!contextMenuTriggerId.value) return
|
||||
const triggerId = contextMenuTriggerId.value
|
||||
contextMenuTriggerId.value = null
|
||||
focusNodeElement(triggerId)
|
||||
}
|
||||
const handleMenuKeydown = (event: KeyboardEvent) => {
|
||||
if (!contextMenu.value.show || !contextMenuRef.value) return
|
||||
const items = Array.from(contextMenuRef.value.querySelectorAll('.menu-item[tabindex="0"]')) as HTMLElement[]
|
||||
if (items.length === 0) return
|
||||
const currentIndex = items.findIndex(item => item === document.activeElement)
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
const next = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length
|
||||
items[next].focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
const prev = currentIndex < 0 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length
|
||||
items[prev].focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Home') {
|
||||
event.preventDefault()
|
||||
items[0].focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'End') {
|
||||
event.preventDefault()
|
||||
items[items.length - 1].focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
closeContextMenu()
|
||||
return
|
||||
}
|
||||
if ((event.key === 'Enter' || event.key === ' ') && document.activeElement instanceof HTMLElement) {
|
||||
event.preventDefault()
|
||||
document.activeElement.click()
|
||||
}
|
||||
}
|
||||
const handleNodeFocus = (id: string, node: TreeNode) => {
|
||||
focusedId.value = id
|
||||
if (!isSelected(id)) {
|
||||
handleSelect(id, node)
|
||||
}
|
||||
}
|
||||
const handleTreeKeydown = (event: KeyboardEvent, node: TreeNode, level: number) => {
|
||||
const { key } = event
|
||||
if (key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
moveFocusByOffset(1)
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
moveFocusByOffset(-1)
|
||||
return
|
||||
}
|
||||
if (key === 'Home') {
|
||||
event.preventDefault()
|
||||
focusFirstVisibleNode()
|
||||
return
|
||||
}
|
||||
if (key === 'End') {
|
||||
event.preventDefault()
|
||||
focusLastVisibleNode()
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
if (hasChildren(node)) {
|
||||
if (!isExpanded(node.id)) {
|
||||
handleToggle(node.id)
|
||||
} else {
|
||||
const currentIndex = getVisibleNodeIndex(node.id)
|
||||
const nextNode = flattenedNodes.value[currentIndex + 1]
|
||||
if (nextNode && nextNode.level === level + 1) {
|
||||
setActiveNode(nextNode.node.id, true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
if (hasChildren(node) && isExpanded(node.id)) {
|
||||
handleToggle(node.id)
|
||||
} else {
|
||||
const parentNode = findParentNode(mockTreeData.value, node.id)
|
||||
if (parentNode) {
|
||||
setActiveNode(parentNode.id, true, true)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleSelect(node.id, node)
|
||||
return
|
||||
}
|
||||
if (key === ' ') {
|
||||
event.preventDefault()
|
||||
if (hasChildren(node)) {
|
||||
handleToggle(node.id)
|
||||
} else {
|
||||
handleSelect(node.id, node)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key === 'ContextMenu' || (key === 'F10' && event.shiftKey)) {
|
||||
event.preventDefault()
|
||||
handleSelect(node.id, node)
|
||||
openContextMenuFromKeyboard(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
const hasChildren = (node: TreeNode) => node.children && node.children.length > 0
|
||||
// 终端节点:只根据显式标记的isTerminal判断
|
||||
|
|
@ -896,10 +1111,15 @@ const handleContextMenu = (e: MouseEvent, nodeId: string) => {
|
|||
y,
|
||||
nodeId
|
||||
}
|
||||
contextMenuTriggerId.value = nodeId
|
||||
}
|
||||
|
||||
const closeContextMenu = () => {
|
||||
const closeContextMenu = (restoreFocus = true) => {
|
||||
const wasOpen = contextMenu.value.show
|
||||
contextMenu.value.show = false
|
||||
if (wasOpen && restoreFocus) {
|
||||
focusNodeAfterContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const openOperationDialog = (mode: 'copy' | 'clone' | 'branch') => {
|
||||
|
|
@ -914,7 +1134,7 @@ const openOperationDialog = (mode: 'copy' | 'clone' | 'branch') => {
|
|||
owner: currentNode?.owner || 'admin'
|
||||
}
|
||||
dialogExpandedIds.value = new Set(expandedIds.value)
|
||||
closeContextMenu()
|
||||
closeContextMenu(false)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
@ -929,8 +1149,15 @@ const handleDelete = () => {
|
|||
if (selectedId.value === nodeId) {
|
||||
selectedId.value = null
|
||||
}
|
||||
const fallbackId = selectedId.value && getNodeById(selectedId.value)
|
||||
? selectedId.value
|
||||
: flattenedNodes.value[0]?.node.id
|
||||
if (fallbackId) {
|
||||
setActiveNode(fallbackId, true, true)
|
||||
}
|
||||
contextMenuTriggerId.value = null
|
||||
}
|
||||
closeContextMenu()
|
||||
closeContextMenu(false)
|
||||
}
|
||||
|
||||
// 添加同级节点
|
||||
|
|
@ -959,7 +1186,7 @@ const handleAddSibling = () => {
|
|||
}
|
||||
console.log('addNodeDialog set to show')
|
||||
|
||||
closeContextMenu()
|
||||
closeContextMenu(false)
|
||||
}
|
||||
|
||||
// 添加下级节点
|
||||
|
|
@ -983,7 +1210,7 @@ const handleAddChild = () => {
|
|||
}
|
||||
console.log('addNodeDialog set to show')
|
||||
|
||||
closeContextMenu()
|
||||
closeContextMenu(false)
|
||||
}
|
||||
|
||||
// 查找节点在父节点中的索引位置
|
||||
|
|
@ -1293,6 +1520,49 @@ onMounted(() => {
|
|||
initializeReviewHistory()
|
||||
})
|
||||
|
||||
watch(() => contextMenu.value.show, (show) => {
|
||||
if (show) {
|
||||
nextTick(() => {
|
||||
contextMenuRef.value?.focus()
|
||||
focusFirstMenuItem()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => addNodeDialog.value.show, (show) => {
|
||||
if (show) {
|
||||
nextTick(() => {
|
||||
addNodeDialogRef.value?.focus()
|
||||
})
|
||||
} else if (contextMenuTriggerId.value) {
|
||||
focusNodeAfterContextMenu()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => operationDialog.value.show, (show) => {
|
||||
if (show) {
|
||||
nextTick(() => {
|
||||
operationDialogRef.value?.focus()
|
||||
})
|
||||
} else if (contextMenuTriggerId.value) {
|
||||
focusNodeAfterContextMenu()
|
||||
}
|
||||
})
|
||||
|
||||
watch(flattenedNodes, (nodes) => {
|
||||
if (nodes.length === 0) {
|
||||
focusedId.value = null
|
||||
selectedId.value = null
|
||||
return
|
||||
}
|
||||
if (focusedId.value && !nodes.some(item => item.node.id === focusedId.value)) {
|
||||
focusedId.value = nodes[0].node.id
|
||||
}
|
||||
if (selectedId.value && !nodes.some(item => item.node.id === selectedId.value)) {
|
||||
selectedId.value = focusedId.value || nodes[0].node.id
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
updateNodeContent
|
||||
|
|
@ -1309,15 +1579,15 @@ defineExpose({
|
|||
<span>Vehicle level - Default(modified)</span>
|
||||
</div>
|
||||
<div class="tree-header-actions">
|
||||
<button class="tree-header-btn" title="Expand All" @click="expandAll">
|
||||
<button class="tree-header-btn" title="Expand All" aria-label="Expand all nodes" @click="expandAll">
|
||||
<Plus :size="12" />
|
||||
</button>
|
||||
<button class="tree-header-btn" title="Collapse All" @click="collapseAll">
|
||||
<button class="tree-header-btn" title="Collapse All" aria-label="Collapse all nodes" @click="collapseAll">
|
||||
<Minus :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree-content">
|
||||
<div ref="treeRootRef" class="tree-content" role="tree" aria-label="Document tree">
|
||||
<div class="tree-column-headers">
|
||||
<span class="tree-col-name">Name</span>
|
||||
<span class="tree-col-status">Status</span>
|
||||
|
|
@ -1325,13 +1595,21 @@ defineExpose({
|
|||
</div>
|
||||
<transition-group name="tree-row" tag="div" class="tree-nodes">
|
||||
<div
|
||||
v-for="{ node, level } in flattenedNodes"
|
||||
v-for="({ node, level }, index) in flattenedNodes"
|
||||
:key="node.id"
|
||||
:class="['tree-node-wrapper', { 'has-children': hasChildren(node) }]"
|
||||
>
|
||||
<div
|
||||
:class="['tree-node', { selected: isSelected(node.id) }]"
|
||||
:data-node-id="node.id"
|
||||
role="treeitem"
|
||||
:aria-level="level + 1"
|
||||
:aria-selected="isSelected(node.id)"
|
||||
:aria-expanded="hasChildren(node) ? isExpanded(node.id) : undefined"
|
||||
:tabindex="isTabStop(node.id, index) ? 0 : -1"
|
||||
@click="handleSelect(node.id, node)"
|
||||
@focus="handleNodeFocus(node.id, node)"
|
||||
@keydown="handleTreeKeydown($event, node, level)"
|
||||
@dblclick="handleDoubleClick(node.id, node)"
|
||||
@contextmenu="handleContextMenu($event, node.id)"
|
||||
>
|
||||
|
|
@ -1339,11 +1617,18 @@ defineExpose({
|
|||
class="tree-node-main"
|
||||
:style="{ paddingLeft: `${level * 16 + 4}px` }"
|
||||
>
|
||||
<span class="tree-node-toggle" @click.stop="handleToggle(node.id)">
|
||||
<button
|
||||
v-if="hasChildren(node)"
|
||||
type="button"
|
||||
class="tree-node-toggle"
|
||||
:aria-label="isExpanded(node.id) ? 'Collapse node' : 'Expand node'"
|
||||
:aria-expanded="isExpanded(node.id)"
|
||||
@click.stop="handleToggle(node.id)"
|
||||
>
|
||||
<Minus v-if="hasChildren(node) && isExpanded(node.id)" :size="12" />
|
||||
<Plus v-else-if="hasChildren(node)" :size="12" />
|
||||
<span v-else class="tree-node-spacer" />
|
||||
</span>
|
||||
</button>
|
||||
<span v-else class="tree-node-spacer" />
|
||||
<span class="tree-node-icon">
|
||||
<Link v-if="isMappedNode(node)" :size="14" class="icon-mapped" />
|
||||
<FileText v-else-if="isTerminalNode(node)" :size="14" class="icon-terminal" />
|
||||
|
|
@ -1384,44 +1669,48 @@ defineExpose({
|
|||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div v-if="contextMenu.show" class="context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||
@click.stop>
|
||||
<div v-if="contextMenu.show" ref="contextMenuRef" class="context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||
role="menu" tabindex="-1" @keydown="handleMenuKeydown" @click.stop>
|
||||
<div class="menu-group">
|
||||
<div class="menu-item" @click="handleAddSibling">
|
||||
<div class="menu-item" role="menuitem" tabindex="0" @click="handleAddSibling">
|
||||
<CornerUpLeft :size="14" />
|
||||
<span>添加同级节点</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="handleAddChild">
|
||||
<div class="menu-item" role="menuitem" tabindex="0" @click="handleAddChild">
|
||||
<CornerDownLeft :size="14" />
|
||||
<span>添加下级节点</span>
|
||||
</div>
|
||||
<div class="menu-item delete" @click="handleDelete">
|
||||
<div class="menu-item delete" role="menuitem" tabindex="0" @click="handleDelete">
|
||||
<Trash2 :size="14" />
|
||||
<span>edit</span>
|
||||
</div>
|
||||
<div class="menu-item delete" @click="handleDelete">
|
||||
<div class="menu-item delete" role="menuitem" tabindex="0" @click="handleDelete">
|
||||
<Trash2 :size="14" />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-item" :class="{ disabled: !isCopyEnabled(contextMenuNode) }"
|
||||
<div class="menu-item" :class="{ disabled: !isCopyEnabled(contextMenuNode) }" role="menuitem"
|
||||
:tabindex="isCopyEnabled(contextMenuNode) ? 0 : -1"
|
||||
@click="isCopyEnabled(contextMenuNode) && openOperationDialog('copy')">
|
||||
<Copy :size="14" />
|
||||
<span>添加文档 (Copy From)...</span>
|
||||
</div>
|
||||
<div class="menu-item" :class="{ disabled: !isCloneEnabled(contextMenuNode) }"
|
||||
<div class="menu-item" :class="{ disabled: !isCloneEnabled(contextMenuNode) }" role="menuitem"
|
||||
:tabindex="isCloneEnabled(contextMenuNode) ? 0 : -1"
|
||||
@click="isCloneEnabled(contextMenuNode) && openOperationDialog('clone')">
|
||||
<Database :size="14" />
|
||||
<span>添加文档 (Clone From)...</span>
|
||||
</div>
|
||||
<div class="menu-item" :class="{ disabled: !isBranchEnabled(contextMenuNode) }"
|
||||
<div class="menu-item" :class="{ disabled: !isBranchEnabled(contextMenuNode) }" role="menuitem"
|
||||
:tabindex="isBranchEnabled(contextMenuNode) ? 0 : -1"
|
||||
@click="isBranchEnabled(contextMenuNode) && openOperationDialog('branch')">
|
||||
<GitBranch :size="14" />
|
||||
<span>Branch From...</span>
|
||||
</div>
|
||||
<div class="menu-item" :class="{ disabled: !isMapEnabled(contextMenuNode) }"
|
||||
<div class="menu-item" :class="{ disabled: !isMapEnabled(contextMenuNode) }" role="menuitem"
|
||||
:tabindex="isMapEnabled(contextMenuNode) ? 0 : -1"
|
||||
@click="isMapEnabled(contextMenuNode) && handleMapTo()">
|
||||
<Plus :size="14" />
|
||||
<span>Map To...</span>
|
||||
|
|
@ -1431,10 +1720,10 @@ defineExpose({
|
|||
|
||||
<!-- 添加节点对话框 -->
|
||||
<div v-if="addNodeDialog.show" class="dialog-overlay" @click="addNodeDialog.show = false">
|
||||
<div class="add-node-dialog" @click.stop>
|
||||
<div ref="addNodeDialogRef" class="add-node-dialog" role="dialog" aria-modal="true" aria-label="Add node dialog" tabindex="-1" @keydown.esc="addNodeDialog.show = false" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h3>{{ addNodeDialog.mode === 'sibling' ? '添加同级节点' : '添加下级节点' }}</h3>
|
||||
<button class="close-btn" @click="addNodeDialog.show = false">
|
||||
<button class="close-btn" aria-label="Close dialog" @click="addNodeDialog.show = false">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1503,11 +1792,11 @@ defineExpose({
|
|||
|
||||
<!-- 操作选择弹窗 -->
|
||||
<div v-if="operationDialog.show" class="dialog-overlay" @click="operationDialog.show = false">
|
||||
<div class="operation-dialog" @click.stop>
|
||||
<div ref="operationDialogRef" class="operation-dialog" role="dialog" aria-modal="true" aria-label="Operation dialog" tabindex="-1" @keydown.esc="operationDialog.show = false" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h3>{{ operationDialog.mode === 'copy' ? 'Copy Node' : operationDialog.mode === 'clone' ? 'Clone Node' :
|
||||
'Branch Node' }}</h3>
|
||||
<button class="close-btn" @click="operationDialog.show = false">
|
||||
<button class="close-btn" aria-label="Close dialog" @click="operationDialog.show = false">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1566,6 +1855,7 @@ defineExpose({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sr-only" aria-live="polite" aria-atomic="true">{{ liveMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -1710,6 +2000,13 @@ defineExpose({
|
|||
user-select: none;
|
||||
font-size: 12px;
|
||||
will-change: background-color, box-shadow, color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree-node:focus-visible {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(0, 120, 212, 0.9),
|
||||
0 0 0 4px rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.tree-node.selected {
|
||||
|
|
@ -1750,12 +2047,21 @@ defineExpose({
|
|||
margin-right: 2px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
transition:
|
||||
background-color 160ms ease-out,
|
||||
color 160ms ease-out,
|
||||
transform 220ms ease-out;
|
||||
}
|
||||
|
||||
.tree-node-toggle:focus-visible {
|
||||
outline: 2px solid #0078d4;
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tree-node:hover .tree-node-toggle {
|
||||
color: #005a9e;
|
||||
transform: scale(1.18);
|
||||
|
|
@ -1992,6 +2298,12 @@ defineExpose({
|
|||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.menu-item:focus-visible {
|
||||
outline: 2px solid #0078d4;
|
||||
outline-offset: -2px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.menu-item.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
|
@ -2404,4 +2716,16 @@ defineExpose({
|
|||
.node-owner-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue