From c77e6f54ee066be83cd6b58d0bedbfbb2fbf8a42 Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 12:55:02 +0100 Subject: [PATCH 01/12] feat(#50): make menu toggle keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 24 ++++++++++++++++++++++++ src/potreeViewer.js | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 src/Accessibility/makeMenuTabbable.js diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js new file mode 100644 index 0000000..9279af1 --- /dev/null +++ b/src/Accessibility/makeMenuTabbable.js @@ -0,0 +1,24 @@ +/** + * Makes menu tabbable keyboard accessible. + */ +export function makeMenuTabbable() { + makeMenuToggleTabbable(); +} + +/** + * Makes menu toggle tabbable and adds keyboard event listeners. + */ +function makeMenuToggleTabbable() { + const quickButtonsContainer = document.getElementById('potree_quick_buttons'); + if (!quickButtonsContainer) return; + const toggle = quickButtonsContainer.querySelector('.potree_menu_toggle'); + if (toggle) { + toggle.tabIndex = 0; + toggle.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggle.click(); + } + }); + } +} \ No newline at end of file diff --git a/src/potreeViewer.js b/src/potreeViewer.js index ad7a0dc..a6f7bf0 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,6 +1,7 @@ import { initAnnotationsPanel } from './AnnotationControl/annotationPanel.js' import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' import { initMiniMap } from './MiniMap/miniMap.js' +import { makeMenuTabbable } from './Accessibility/makeMenuTabbable.js' import { initThreePanels, toggleAcceptedLegend @@ -125,6 +126,8 @@ export async function createPotreeViewer( makeGlobeBackgroundOption() + makeMenuTabbable() + initMeasurementsPanel(viewer) initAnnotationsPanel(viewer) initMiniMap(viewer) From 090da51d0d189196fb8085be4e987c408df62a6b Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 12:58:37 +0100 Subject: [PATCH 02/12] feat(#50): make minimap keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 9279af1..001a209 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -3,6 +3,7 @@ */ export function makeMenuTabbable() { makeMenuToggleTabbable(); + makeMiniMapTabbable(); } /** @@ -21,4 +22,22 @@ function makeMenuToggleTabbable() { } }); } +} + +/** + * Makes minimap tabbable and adds keyboard event listeners. + */ +function makeMiniMapTabbable() { + const quickButtonsContainer = document.getElementById('potree_quick_buttons'); + if (!quickButtonsContainer) return; + const toggle = quickButtonsContainer.querySelector('#potree_map_toggle'); + if (toggle) { + toggle.tabIndex = 0; + toggle.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggle.click(); + } + }); + } } \ No newline at end of file From 8189a4000aaf690f59b950c496743e950c3b24b7 Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 13:02:54 +0100 Subject: [PATCH 03/12] feat(#50): make panels keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 001a209..79502e5 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -2,8 +2,9 @@ * Makes menu tabbable keyboard accessible. */ export function makeMenuTabbable() { - makeMenuToggleTabbable(); - makeMiniMapTabbable(); + makeMenuToggleTabbable() + makeMiniMapTabbable() + makePanelsTabbable() } /** @@ -25,7 +26,7 @@ function makeMenuToggleTabbable() { } /** - * Makes minimap tabbable and adds keyboard event listeners. + * Makes minimap tabbable and keyboard clickable */ function makeMiniMapTabbable() { const quickButtonsContainer = document.getElementById('potree_quick_buttons'); @@ -40,4 +41,23 @@ function makeMiniMapTabbable() { } }); } +} + +/** + * Makes accordion titles tabbable and keyboard clickable + */ +function makePanelsTabbable() { + const menu = document.getElementById('potree_menu'); + if (menu) { + const headers = menu.querySelectorAll('h3'); + headers.forEach((header) => { + header.tabIndex = 0; + header.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + header.click(); + } + }); + }); + } } \ No newline at end of file From ed883b36c35c7f7a9b604d0103eab35b52123a1d Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 13:27:22 +0100 Subject: [PATCH 04/12] feat(#50): make elevation panel keyboard accessible --- src/AcceptedFiltering/threePanels.js | 4 +-- src/Accessibility/makeMenuTabbable.js | 42 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/AcceptedFiltering/threePanels.js b/src/AcceptedFiltering/threePanels.js index 249df0f..25de910 100644 --- a/src/AcceptedFiltering/threePanels.js +++ b/src/AcceptedFiltering/threePanels.js @@ -204,11 +204,11 @@ function setUpElevationSlider(hooks) { const update = () => { const low = slider.slider('values', 0) const high = slider.slider('values', 1) - label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}` + label.textContent = `${Math.round(low)} to ${Math.round(high)}` hooks?.onElevationRangeChange([low, high]) } - slider.slider({ min: -10000, max: 0, values: [-10000, 0] }) + slider.slider({ min: -10000, max: 0, values: [-10000, 0], step: 1 }) slider.off('slide.custom slidestop.custom change.custom') slider.on('slide.custom slidestop.custom change.custom', update) update() diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 79502e5..b019647 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -5,6 +5,7 @@ export function makeMenuTabbable() { makeMenuToggleTabbable() makeMiniMapTabbable() makePanelsTabbable() + makeElevationControlTabbable() } /** @@ -60,4 +61,45 @@ function makePanelsTabbable() { }); }); } +} + +/** + * Makes elevation control panel tabbable and keyboard clickable + */ +function makeElevationControlTabbable() { + // Make activate elevation control button tabbable and keyboard clickable + const activateButton = document.getElementById('btnDoElevationControl') + if (activateButton) { + activateButton.tabIndex = 0; + activateButton.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + activateButton.click(); + } + }); + } + + // Make gradient clamp, repeat and mirrored repeat buttons tabbable and keyboard clickable + const gradientRadios = document.querySelectorAll('#gradient_repeat_option label'); + gradientRadios.forEach((radio) => { + radio.tabIndex = 0; + radio.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + radio.click(); + } + }); + }); + + // Make gradient schemes tabbable and keyboard clickable + const gradientSpans = document.querySelectorAll('#elevation_gradient_scheme_selection span'); + gradientSpans.forEach((span) => { + span.tabIndex = 0; + span.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + span.click(); + } + }); + }); } \ No newline at end of file From e6c6a30bd11ab5d3bf1a83132f4a1dc1a1c496a5 Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 13:30:05 +0100 Subject: [PATCH 05/12] feat(#50): make accepted filtering keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index b019647..7153559 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -6,6 +6,7 @@ export function makeMenuTabbable() { makeMiniMapTabbable() makePanelsTabbable() makeElevationControlTabbable() + makeAcceptedFilteringTabbable() } /** @@ -102,4 +103,20 @@ function makeElevationControlTabbable() { } }); }); +} + +/** + * Makes accepted filtering panel tabbable and keyboard clickable + */ +function makeAcceptedFilteringTabbable() { + const activateButton = document.getElementById('doAcceptedFiltering') + if (activateButton) { + activateButton.tabIndex = 0; + activateButton.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + activateButton.click(); + } + }); + } } \ No newline at end of file From 89a736d6f405f7fbb7722fb202d0630429cdebe1 Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 13:36:49 +0100 Subject: [PATCH 06/12] fix(#50): make activate buttons not loose focus on click --- src/Accessibility/makeMenuTabbable.js | 90 ++++++++++++++------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 7153559..c969dba 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -13,17 +13,17 @@ export function makeMenuTabbable() { * Makes menu toggle tabbable and adds keyboard event listeners. */ function makeMenuToggleTabbable() { - const quickButtonsContainer = document.getElementById('potree_quick_buttons'); - if (!quickButtonsContainer) return; - const toggle = quickButtonsContainer.querySelector('.potree_menu_toggle'); + const quickButtonsContainer = document.getElementById('potree_quick_buttons') + if (!quickButtonsContainer) return + const toggle = quickButtonsContainer.querySelector('.potree_menu_toggle') if (toggle) { - toggle.tabIndex = 0; + toggle.tabIndex = 0 toggle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - toggle.click(); + e.preventDefault() + toggle.click() } - }); + }) } } @@ -31,17 +31,17 @@ function makeMenuToggleTabbable() { * Makes minimap tabbable and keyboard clickable */ function makeMiniMapTabbable() { - const quickButtonsContainer = document.getElementById('potree_quick_buttons'); - if (!quickButtonsContainer) return; - const toggle = quickButtonsContainer.querySelector('#potree_map_toggle'); + const quickButtonsContainer = document.getElementById('potree_quick_buttons') + if (!quickButtonsContainer) return + const toggle = quickButtonsContainer.querySelector('#potree_map_toggle') if (toggle) { - toggle.tabIndex = 0; + toggle.tabIndex = 0 toggle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - toggle.click(); + e.preventDefault() + toggle.click() } - }); + }) } } @@ -49,18 +49,18 @@ function makeMiniMapTabbable() { * Makes accordion titles tabbable and keyboard clickable */ function makePanelsTabbable() { - const menu = document.getElementById('potree_menu'); + const menu = document.getElementById('potree_menu') if (menu) { - const headers = menu.querySelectorAll('h3'); + const headers = menu.querySelectorAll('h3') headers.forEach((header) => { - header.tabIndex = 0; + header.tabIndex = 0 header.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - header.click(); + e.preventDefault() + header.click() } - }); - }); + }) + }) } } @@ -71,38 +71,43 @@ function makeElevationControlTabbable() { // Make activate elevation control button tabbable and keyboard clickable const activateButton = document.getElementById('btnDoElevationControl') if (activateButton) { - activateButton.tabIndex = 0; + activateButton.tabIndex = 0 activateButton.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - activateButton.click(); + e.preventDefault() + activateButton.click() + activateButton.focus() } - }); + }) } // Make gradient clamp, repeat and mirrored repeat buttons tabbable and keyboard clickable - const gradientRadios = document.querySelectorAll('#gradient_repeat_option label'); + const gradientRadios = document.querySelectorAll( + '#gradient_repeat_option label' + ) gradientRadios.forEach((radio) => { - radio.tabIndex = 0; + radio.tabIndex = 0 radio.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - radio.click(); + e.preventDefault() + radio.click() } - }); - }); + }) + }) // Make gradient schemes tabbable and keyboard clickable - const gradientSpans = document.querySelectorAll('#elevation_gradient_scheme_selection span'); + const gradientSpans = document.querySelectorAll( + '#elevation_gradient_scheme_selection span' + ) gradientSpans.forEach((span) => { - span.tabIndex = 0; + span.tabIndex = 0 span.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - span.click(); + e.preventDefault() + span.click() } - }); - }); + }) + }) } /** @@ -111,12 +116,13 @@ function makeElevationControlTabbable() { function makeAcceptedFilteringTabbable() { const activateButton = document.getElementById('doAcceptedFiltering') if (activateButton) { - activateButton.tabIndex = 0; + activateButton.tabIndex = 0 activateButton.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - activateButton.click(); + e.preventDefault() + activateButton.click() + activateButton.focus() } - }); + }) } -} \ No newline at end of file +} From d51c4aa642dc71ba2b873ccf409452d1a72dfe6a Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 29 Oct 2025 14:48:40 +0100 Subject: [PATCH 07/12] feat(#50): make appearance panel keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 80 +++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index c969dba..7e641da 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -7,6 +7,7 @@ export function makeMenuTabbable() { makePanelsTabbable() makeElevationControlTabbable() makeAcceptedFilteringTabbable() + makeAppearancePanelTabbable() } /** @@ -82,15 +83,15 @@ function makeElevationControlTabbable() { } // Make gradient clamp, repeat and mirrored repeat buttons tabbable and keyboard clickable - const gradientRadios = document.querySelectorAll( + const gradientButtons = document.querySelectorAll( '#gradient_repeat_option label' ) - gradientRadios.forEach((radio) => { - radio.tabIndex = 0 - radio.addEventListener('keydown', (e) => { + gradientButtons.forEach((label) => { + label.tabIndex = 0 + label.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - radio.click() + label.click() } }) }) @@ -126,3 +127,72 @@ function makeAcceptedFilteringTabbable() { }) } } + +/** + * Makes appearance panel tabbable and keyboard clickable + */ +function makeAppearancePanelTabbable() { + // Make EDL checkbox tabbable and keyboard clickable + const edlCheckbox = document.getElementById('chkEDLEnabled'); + if (edlCheckbox) { + edlCheckbox.tabIndex = 0 + edlCheckbox.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + edlCheckbox.click(); + } + }); + } + + // Make background buttons tabbable and keyboard clickable + const backgroundButtons = document.querySelectorAll( + '#background_options label' + ) + backgroundButtons.forEach((label) => { + label.tabIndex = 0 + label.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + label.click() + } + }) + }) + + // Make splat quality buttons tabbable and keyboard clickable + const splatQualityButtons = document.querySelectorAll( + '#splat_quality_options label' + ) + splatQualityButtons.forEach((label) => { + label.tabIndex = 0 + label.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + label.click() + } + }) + }) + + // Make box checkbox tabbable and keyboard clickable + const boxCheckbox = document.getElementById('show_bounding_box'); + if (boxCheckbox) { + boxCheckbox.tabIndex = 0 + boxCheckbox.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + boxCheckbox.click(); + } + }); + } + + // Make lock view checkbox tabbable and keyboard clickable + const lockViewCheckBox = document.getElementById('set_freeze'); + if (lockViewCheckBox) { + lockViewCheckBox.tabIndex = 0 + lockViewCheckBox.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + lockViewCheckBox.click(); + } + }); + } +} From 9958f8679f5f86b2a1ab9c4112e63bc474a7d4b8 Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Sun, 2 Nov 2025 21:48:43 +0100 Subject: [PATCH 08/12] feat(#50): make tools panel keyboard accessible --- src/Accessibility/makeMenuTabbable.js | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 7e641da..7e34e52 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -8,6 +8,7 @@ export function makeMenuTabbable() { makeElevationControlTabbable() makeAcceptedFilteringTabbable() makeAppearancePanelTabbable() + makeToolsPanelTabbable() } /** @@ -196,3 +197,73 @@ function makeAppearancePanelTabbable() { }); } } + +/** + * Makes tools panel tabbable and keyboard clickable + */ +function makeToolsPanelTabbable() { + // Make clipping tools tabbable and keyboard clickable + const clippingTools = document.querySelectorAll( + '#clipping_tools img' + ) + clippingTools.forEach((img, index) => { + // Hide unsupported tool + if (index === 2) { + img.hidden = true + return + } + img.tabIndex = 0 + img.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + img.click() + } + }) + }) + + // Make clip task buttons tabbable and keyboard clickable + const clipTaskButtons = document.querySelectorAll( + '#cliptask_options label' + ) + clipTaskButtons.forEach((label) => { + label.tabIndex = 0 + label.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + label.click() + } + }) + }) + + // Make clip method buttons tabbable and keyboard clickable + const clipMethodButtons = document.querySelectorAll( + '#clipmethod_options label' + ) + clipMethodButtons.forEach((label) => { + label.tabIndex = 0 + label.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + label.click() + } + }) + }) + + // Hide camera projection options as they are not supported + const cameraProjection = document.getElementById('camera_projection_options'); + cameraProjection.hidden = true + + // Make navigation tools tabbable and keyboard clickable + const navigationTools = document.querySelectorAll( + '#navigation img' + ) + navigationTools.forEach((img) => { + img.tabIndex = 0 + img.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + img.click() + } + }) + }) +} From 3a999557f6ddedd6877fb5b89924c0d998b2abe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 3 Nov 2025 14:07:13 +0100 Subject: [PATCH 09/12] fix(#50): :bug: Selecting a navigation tool with tab should work --- src/Accessibility/makeMenuTabbable.js | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 7e34e52..2d3ce43 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -251,18 +251,58 @@ function makeToolsPanelTabbable() { // Hide camera projection options as they are not supported const cameraProjection = document.getElementById('camera_projection_options'); - cameraProjection.hidden = true + if (cameraProjection) cameraProjection.hidden = true // Make navigation tools tabbable and keyboard clickable const navigationTools = document.querySelectorAll( '#navigation img' ) + const renderArea = document.querySelector('#potree_render_area') + // Keep wrapper focusable as a fallback target (doesn't change tab order elsewhere) + if (renderArea && !renderArea.hasAttribute('tabindex')) { + renderArea.setAttribute('tabindex', '0') + } + navigationTools.forEach((img) => { img.tabIndex = 0 img.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() img.click() + + // Switch controls directly via Potree API, then focus the keyboard input target (canvas) + const pv = window.potreeViewer || window.viewer || null + const target = renderArea + if (pv) { + const src = (img.getAttribute && img.getAttribute('src')) || '' + const title = img.getAttribute && (img.getAttribute('title') || img.getAttribute('data-i18n') || '') + const key = (src + ' ' + title).toLowerCase() + + if (key.includes('earth')) { + pv.setControls(pv.earthControls) + } else if (key.includes('heli') || key.includes('helicopter')) { + pv.setControls(pv.fpControls) + if (pv.fpControls) pv.fpControls.lockElevation = true + } else if (key.includes('fps') || key.includes('flight') || key.includes('flight_control')) { + pv.setControls(pv.fpControls) + if (pv.fpControls) pv.fpControls.lockElevation = false + } else if (key.includes('orbit')) { + pv.setControls(pv.orbitControls) + } + + const dom = pv && pv.renderer && pv.renderer.domElement + if (dom) { + if (!dom.hasAttribute('tabindex')) dom.setAttribute('tabindex', '-1') + dom.focus() + } else if (target) { + // conservative fallback + if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '0') + target.focus() + } + } else if (target) { + if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '0') + target.focus() + } } }) }) From 5e34abd1db1d20bdd6486d5557788d7f1848a1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Tue, 4 Nov 2025 11:06:29 +0100 Subject: [PATCH 10/12] feat(#50): :sparkles: Make measurment panel tabbable --- src/Accessibility/makeMenuTabbable.js | 75 +++++++++++++++++---------- src/potreeViewer.js | 6 ++- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 2d3ce43..1dfa94d 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -7,6 +7,7 @@ export function makeMenuTabbable() { makePanelsTabbable() makeElevationControlTabbable() makeAcceptedFilteringTabbable() + makeMeasurementsTabbable() makeAppearancePanelTabbable() makeToolsPanelTabbable() } @@ -134,15 +135,15 @@ function makeAcceptedFilteringTabbable() { */ function makeAppearancePanelTabbable() { // Make EDL checkbox tabbable and keyboard clickable - const edlCheckbox = document.getElementById('chkEDLEnabled'); + const edlCheckbox = document.getElementById('chkEDLEnabled') if (edlCheckbox) { edlCheckbox.tabIndex = 0 edlCheckbox.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - edlCheckbox.click(); + e.preventDefault() + edlCheckbox.click() } - }); + }) } // Make background buttons tabbable and keyboard clickable @@ -174,27 +175,27 @@ function makeAppearancePanelTabbable() { }) // Make box checkbox tabbable and keyboard clickable - const boxCheckbox = document.getElementById('show_bounding_box'); + const boxCheckbox = document.getElementById('show_bounding_box') if (boxCheckbox) { boxCheckbox.tabIndex = 0 boxCheckbox.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - boxCheckbox.click(); + e.preventDefault() + boxCheckbox.click() } - }); + }) } // Make lock view checkbox tabbable and keyboard clickable - const lockViewCheckBox = document.getElementById('set_freeze'); + const lockViewCheckBox = document.getElementById('set_freeze') if (lockViewCheckBox) { lockViewCheckBox.tabIndex = 0 lockViewCheckBox.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - lockViewCheckBox.click(); + e.preventDefault() + lockViewCheckBox.click() } - }); + }) } } @@ -203,9 +204,7 @@ function makeAppearancePanelTabbable() { */ function makeToolsPanelTabbable() { // Make clipping tools tabbable and keyboard clickable - const clippingTools = document.querySelectorAll( - '#clipping_tools img' - ) + const clippingTools = document.querySelectorAll('#clipping_tools img') clippingTools.forEach((img, index) => { // Hide unsupported tool if (index === 2) { @@ -222,9 +221,7 @@ function makeToolsPanelTabbable() { }) // Make clip task buttons tabbable and keyboard clickable - const clipTaskButtons = document.querySelectorAll( - '#cliptask_options label' - ) + const clipTaskButtons = document.querySelectorAll('#cliptask_options label') clipTaskButtons.forEach((label) => { label.tabIndex = 0 label.addEventListener('keydown', (e) => { @@ -250,13 +247,11 @@ function makeToolsPanelTabbable() { }) // Hide camera projection options as they are not supported - const cameraProjection = document.getElementById('camera_projection_options'); + const cameraProjection = document.getElementById('camera_projection_options') if (cameraProjection) cameraProjection.hidden = true // Make navigation tools tabbable and keyboard clickable - const navigationTools = document.querySelectorAll( - '#navigation img' - ) + const navigationTools = document.querySelectorAll('#navigation img') const renderArea = document.querySelector('#potree_render_area') // Keep wrapper focusable as a fallback target (doesn't change tab order elsewhere) if (renderArea && !renderArea.hasAttribute('tabindex')) { @@ -275,7 +270,9 @@ function makeToolsPanelTabbable() { const target = renderArea if (pv) { const src = (img.getAttribute && img.getAttribute('src')) || '' - const title = img.getAttribute && (img.getAttribute('title') || img.getAttribute('data-i18n') || '') + const title = + img.getAttribute && + (img.getAttribute('title') || img.getAttribute('data-i18n') || '') const key = (src + ' ' + title).toLowerCase() if (key.includes('earth')) { @@ -283,7 +280,11 @@ function makeToolsPanelTabbable() { } else if (key.includes('heli') || key.includes('helicopter')) { pv.setControls(pv.fpControls) if (pv.fpControls) pv.fpControls.lockElevation = true - } else if (key.includes('fps') || key.includes('flight') || key.includes('flight_control')) { + } else if ( + key.includes('fps') || + key.includes('flight') || + key.includes('flight_control') + ) { pv.setControls(pv.fpControls) if (pv.fpControls) pv.fpControls.lockElevation = false } else if (key.includes('orbit')) { @@ -292,18 +293,38 @@ function makeToolsPanelTabbable() { const dom = pv && pv.renderer && pv.renderer.domElement if (dom) { - if (!dom.hasAttribute('tabindex')) dom.setAttribute('tabindex', '-1') + if (!dom.hasAttribute('tabindex')) + dom.setAttribute('tabindex', '-1') dom.focus() } else if (target) { // conservative fallback - if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '0') + if (!target.hasAttribute('tabindex')) + target.setAttribute('tabindex', '0') target.focus() } } else if (target) { - if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '0') + if (!target.hasAttribute('tabindex')) + target.setAttribute('tabindex', '0') target.focus() } } }) }) } + +function makeMeasurementsTabbable() { + // Select every tool inside the measurement tools host + const tools = document.querySelectorAll( + '#measurement_tools_host .tool-with-label' + ) + tools.forEach((tool) => { + tool.tabIndex = 0 + tool.setAttribute('role', 'button') + tool.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + tool.click() + } + }) + }) +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 3dc2656..f16ebcd 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -57,6 +57,8 @@ export async function createPotreeViewer( viewer.loadGUI(() => { viewer.setLanguage('en') + //remove the header with language information + $('#sidebar_header').remove() $('#menu_filters').remove() viewer.toggleSidebar() @@ -127,11 +129,11 @@ export async function createPotreeViewer( // Apply runtime overrides for the 2D Profile tool init2DProfileOverride(viewer) - makeMenuTabbable() - initMeasurementsPanel(viewer) initAnnotationsPanel(viewer) initMiniMap(viewer) + + makeMenuTabbable() }) // Initialize camera position and target point (manually chosen) From 58151097a9333c7aab1b947b9fa081e1172a41af Mon Sep 17 00:00:00 2001 From: AdrianSolberg Date: Wed, 5 Nov 2025 14:02:10 +0100 Subject: [PATCH 11/12] fix(#50): fix globe hiding for navigation controls --- src/cameraSync.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/cameraSync.js b/src/cameraSync.js index cead992..4b40b76 100644 --- a/src/cameraSync.js +++ b/src/cameraSync.js @@ -58,11 +58,7 @@ export function syncCameras(potreeViewer, cesiumViewer) { } /** - * Determines whether the globe should be visible based on the camera position. - * - * Returns false if the camera is below the globe surface, if the pivot - * point would be blocked by the curvature of the Earth or if the camera - * is looking almost straight down at the pivot. + * Determines whether the globe should be visible based on the camera position and controls. * * @param cameraPos - The camera position in Cesium.Cartesian3 coordinates * @param pivot - The pivot point in Cesium.Cartesian3 coordinates @@ -99,7 +95,17 @@ function shouldShowGlobe(cameraPos, pivot, direction) { const targetNormal = Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(pivot) const dotProduct = Math.abs(Cesium.Cartesian3.dot(direction, targetNormal)) - // If camera is "above" pivot on the axis, and not looking nearly straight down, the globe should be visible - // Otherwise, the globe should not be visible - return camProj >= pivotProj && dotProduct < 0.99 + // Get the camera's elevation based on ellipsoid + const cameraCarto = Cesium.Cartographic.fromCartesian(cameraPos, ellipsoid) + const elevation = cameraCarto.height + + // Determine globe visibility based on camera controls + if (window.potreeViewer.getControls() === window.potreeViewer.orbitControls) { + // If camera is "above" pivot on the axis, and not looking nearly straight down, the globe should be visible + // Otherwise, the globe should not be visible + return camProj >= pivotProj && dotProduct < 0.99 + } else { + // If the camera is inside the globe, or looking nearly straight down, hide the globe + return elevation > 0 && dotProduct < 0.99 + } } From ac23d5a33b246d8c6957d396ef20631194aa423f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Fri, 7 Nov 2025 15:41:43 +0100 Subject: [PATCH 12/12] feat(#50): :sparkles: scene is now more tabbable --- src/Accessibility/makeMenuTabbable.js | 162 ++++++++++++++++++++++++++ src/main.js | 1 - 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/Accessibility/makeMenuTabbable.js b/src/Accessibility/makeMenuTabbable.js index 1dfa94d..04821f3 100644 --- a/src/Accessibility/makeMenuTabbable.js +++ b/src/Accessibility/makeMenuTabbable.js @@ -7,6 +7,8 @@ export function makeMenuTabbable() { makePanelsTabbable() makeElevationControlTabbable() makeAcceptedFilteringTabbable() + makeObjectsTabbable() + makeObjectsDropdownsTabbable() makeMeasurementsTabbable() makeAppearancePanelTabbable() makeToolsPanelTabbable() @@ -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 }) + } +} diff --git a/src/main.js b/src/main.js index 87aaafe..45714a4 100644 --- a/src/main.js +++ b/src/main.js @@ -21,7 +21,6 @@ async function init() { POTREE_POINTCLOUD_URLS, POTREE_SETTINGS ) - potreeViewer.addEventListener('camera_changed', updateText) setupRightClickListener(potreeViewer)