Skip to content

Commit

Permalink
Keyboard navigation for interactive array viewer (#2996)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp authored Aug 14, 2024
1 parent 45c0a02 commit 159dddc
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 11 deletions.
19 changes: 15 additions & 4 deletions frontend/common/useEventListener.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { useCallback, useEffect } from "../imports/Preact.js"

/**
* @typedef EventListenerAddable
* @type Document | HTMLElement | Window | EventSource | MediaQueryList | null
*/

export const useEventListener = (
/** @type {Document | HTMLElement | Window | EventSource | MediaQueryList | null} */ element,
/** @type {EventListenerAddable | import("../imports/Preact.js").Ref<EventListenerAddable>} */ element,
/** @type {string} */ event_name,
/** @type {EventListenerOrEventListenerObject} */ handler,
/** @type {any[] | undefined} */ deps
) => {
let handler_cached = useCallback(handler, deps)
useEffect(() => {
if (element == null) return
element.addEventListener(event_name, handler_cached)
return () => element.removeEventListener(event_name, handler_cached)
const e = element
const useme =
e == null || e instanceof Document || e instanceof HTMLElement || e instanceof Window || e instanceof EventSource || e instanceof MediaQueryList
? /** @type {EventListenerAddable} */ (e)
: e.current

if (useme == null) return
useme.addEventListener(event_name, handler_cached)
return () => useme.removeEventListener(event_name, handler_cached)
}, [element, event_name, handler_cached])
}
58 changes: 51 additions & 7 deletions frontend/components/TreeView.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { html, useRef, useState, useContext } from "../imports/Preact.js"
import { html, useRef, useState, useContext, useEffect } from "../imports/Preact.js"

import { OutputBody, PlutoImage } from "./CellOutput.js"
import { PlutoActionsContext } from "../common/PlutoContext.js"
import { useEventListener } from "../common/useEventListener.js"

// this is different from OutputBody because:
// it does not wrap in <div>. We want to do that in OutputBody for reasons that I forgot (feel free to try and remove it), but we dont want it here
Expand Down Expand Up @@ -34,8 +35,13 @@ export const SimpleOutputBody = ({ mime, body, cell_id, persist_js_state, saniti

const More = ({ on_click_more }) => {
const [loading, set_loading] = useState(false)
const element_ref = useRef(/** @type {HTMLElement?} */ (null))
useKeyboardClickable(element_ref)

return html`<pluto-tree-more
ref=${element_ref}
tabindex="0"
role="button"
class=${loading ? "loading" : ""}
onclick=${(e) => {
if (!loading) {
Expand All @@ -48,8 +54,45 @@ const More = ({ on_click_more }) => {
>`
}

const prefix = ({ prefix, prefix_short }) =>
html`<pluto-tree-prefix><span class="long">${prefix}</span><span class="short">${prefix_short}</span></pluto-tree-prefix>`
const useKeyboardClickable = (element_ref) => {
useEventListener(
element_ref,
"keydown",
(e) => {
if (e.key === " ") {
e.preventDefault()
}
if (e.key === "Enter") {
e.preventDefault()
element_ref.current.click()
}
},
[]
)

useEventListener(
element_ref,
"keyup",
(e) => {
if (e.key === " ") {
e.preventDefault()
element_ref.current.click()
}
},
[]
)
}

const prefix = ({ prefix, prefix_short }) => {
const element_ref = useRef(/** @type {HTMLElement?} */ (null))
useEffect(() => {
console.log(element_ref.current)
}, [])
useKeyboardClickable(element_ref)
return html`<pluto-tree-prefix role="button" tabindex="0" ref=${element_ref}
><span class="long">${prefix}</span><span class="short">${prefix_short}</span></pluto-tree-prefix
>`
}

const actions_show_more = ({ pluto_actions, cell_id, node_ref, objectid, dim }) => {
const actions = pluto_actions ?? node_ref.current.closest("pluto-cell")._internal_pluto_actions
Expand All @@ -59,6 +102,7 @@ const actions_show_more = ({ pluto_actions, cell_id, node_ref, objectid, dim })
export const TreeView = ({ mime, body, cell_id, persist_js_state, sanitize_html = true }) => {
let pluto_actions = useContext(PlutoActionsContext)
const node_ref = useRef(/** @type {HTMLElement?} */ (null))

const onclick = (e) => {
// TODO: this could be reactified but no rush
let self = node_ref.current
Expand Down Expand Up @@ -103,28 +147,28 @@ export const TreeView = ({ mime, body, cell_id, persist_js_state, sanitize_html
case "Array":
case "Set":
case "Tuple":
inner = html`${prefix(body)}<pluto-tree-items class=${body.type}
inner = html`<${prefix} prefix=${body.prefix} prefix_short=${body.prefix_short} /><pluto-tree-items class=${body.type}
>${body.elements.map((r) =>
r === "more" ? more : html`<p-r>${body.type === "Set" ? "" : html`<p-k>${r[0]}</p-k>`}<p-v>${mimepair_output(r[1])}</p-v></p-r>`
)}</pluto-tree-items
>`
break
case "Dict":
inner = html`${prefix(body)}<pluto-tree-items class=${body.type}
inner = html`<${prefix} prefix=${body.prefix} prefix_short=${body.prefix_short} /><pluto-tree-items class=${body.type}
>${body.elements.map((r) =>
r === "more" ? more : html`<p-r><p-k>${mimepair_output(r[0])}</p-k><p-v>${mimepair_output(r[1])}</p-v></p-r>`
)}</pluto-tree-items
>`
break
case "NamedTuple":
inner = html`${prefix(body)}<pluto-tree-items class=${body.type}
inner = html`<${prefix} prefix=${body.prefix} prefix_short=${body.prefix_short} /><pluto-tree-items class=${body.type}
>${body.elements.map((r) =>
r === "more" ? more : html`<p-r><p-k>${r[0]}</p-k><p-v>${mimepair_output(r[1])}</p-v></p-r>`
)}</pluto-tree-items
>`
break
case "struct":
inner = html`${prefix(body)}<pluto-tree-items class=${body.type}
inner = html`<${prefix} prefix=${body.prefix} prefix_short=${body.prefix_short} /><pluto-tree-items class=${body.type}
>${body.elements.map((r) => html`<p-r><p-k>${r[0]}</p-k><p-v>${mimepair_output(r[1])}</p-v></p-r>`)}</pluto-tree-items
>`
break
Expand Down

0 comments on commit 159dddc

Please sign in to comment.