Skip to content

Commit

Permalink
feat(#50): ✨ scene is now more tabbable
Browse files Browse the repository at this point in the history
  • Loading branch information
gautegf committed Nov 7, 2025
1 parent 5815109 commit ac23d5a
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 1 deletion.
162 changes: 162 additions & 0 deletions src/Accessibility/makeMenuTabbable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export function makeMenuTabbable() {
makePanelsTabbable()
makeElevationControlTabbable()
makeAcceptedFilteringTabbable()
makeObjectsTabbable()
makeObjectsDropdownsTabbable()
makeMeasurementsTabbable()
makeAppearancePanelTabbable()
makeToolsPanelTabbable()
Expand Down Expand Up @@ -328,3 +330,163 @@ function makeMeasurementsTabbable() {
})
})
}

/**
* Makes the Scene > Objects tree tabbable and keyboard operable
* - Tab to anchors
* - Enter/Space = select (click)
* - ArrowRight = open node, ArrowLeft = close node (uses jsTree if available)
* - ArrowUp/ArrowDown = move focus to previous/next visible item
* - Home/End = first/last visible item
*/
function makeObjectsTabbable() {
const install = (tree) => {
if (!tree) return

// Ensure all anchors are tabbable
const setTabbable = (root) => {
root.querySelectorAll('a.jstree-anchor').forEach((a) => {
a.tabIndex = 0
})
}
setTabbable(tree)

// Keep anchors tabbable when jsTree updates DOM
const mo = new MutationObserver((muts) => {
for (const m of muts) {
if (m.addedNodes && m.addedNodes.length) {
m.addedNodes.forEach((n) => {
if (n.nodeType === 1) setTabbable(n)
})
}
}
})
mo.observe(tree, { childList: true, subtree: true })

const getAnchors = () =>
Array.from(tree.querySelectorAll('a.jstree-anchor'))
const focusMove = (from, dir) => {
const anchors = getAnchors()
const i = anchors.indexOf(from)
const next = anchors[i + dir]
if (next) next.focus()
}
const focusEdge = (toLast) => {
const anchors = getAnchors()
const target = toLast ? anchors[anchors.length - 1] : anchors[0]
if (target) target.focus()
}

// Delegate key handling on anchors
tree.addEventListener('keydown', (e) => {
const a = e.target
if (
!a ||
!(a instanceof HTMLElement) ||
!a.classList.contains('jstree-anchor')
)
return
const li = a.closest('li')
const id = li && li.id
const $ = window.jQuery || window.$
const inst = $ && $(tree).jstree ? $(tree).jstree(true) : null

switch (e.key) {
case 'Enter':
case ' ': // space
e.preventDefault()
a.click()
break
case 'ArrowDown':
e.preventDefault()
focusMove(a, +1)
break
case 'ArrowUp':
e.preventDefault()
focusMove(a, -1)
break
case 'Home':
e.preventDefault()
focusEdge(false)
break
case 'End':
e.preventDefault()
focusEdge(true)
break
case 'ArrowRight':
e.preventDefault()
if (inst && id) {
inst.open_node(id)
} else if (li && li.classList.contains('jstree-closed')) {
const toggle = li.querySelector('.jstree-ocl')
if (toggle)
toggle.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}
break
case 'ArrowLeft':
e.preventDefault()
if (inst && id) {
inst.close_node(id)
} else if (li && li.classList.contains('jstree-open')) {
const toggle = li.querySelector('.jstree-ocl')
if (toggle)
toggle.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}
break
}
})
}

const tryInit = () => {
const tree = document.getElementById('scene_objects')
if (tree) {
install(tree)
return true
}
return false
}

if (!tryInit()) {
// Wait for GUI to load
const obs = new MutationObserver(() => {
if (tryInit()) obs.disconnect()
})
obs.observe(document.body, { childList: true, subtree: true })
}
}

/**
* Make dropdowns in Properties tabbable: focus the associated input/select when label is activated via keyboard.
*/
function makeObjectsDropdownsTabbable() {
const make = () => {
const container = document.getElementById('potree_menu')
if (!container) return false

// Labels that point to inputs/selects
container.querySelectorAll('label[for]').forEach((lbl) => {
lbl.tabIndex = 0
lbl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
const id = lbl.getAttribute('for')
const target = id && document.getElementById(id)
if (target instanceof HTMLElement) target.focus()
}
})
})

// Ensure selects are in tab order (usually default) and openable via keyboard focus
container.querySelectorAll('select').forEach((sel) => {
sel.tabIndex = 0
})
return true
}

if (!make()) {
const obs = new MutationObserver(() => {
if (make()) obs.disconnect()
})
obs.observe(document.body, { childList: true, subtree: true })
}
}
1 change: 0 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ async function init() {
POTREE_POINTCLOUD_URLS,
POTREE_SETTINGS
)

potreeViewer.addEventListener('camera_changed', updateText)
setupRightClickListener(potreeViewer)

Expand Down

0 comments on commit ac23d5a

Please sign in to comment.