diff --git a/src/core/islands/Panel.mjs b/src/core/islands/Panel.mjs index be51419..9e1c29f 100644 --- a/src/core/islands/Panel.mjs +++ b/src/core/islands/Panel.mjs @@ -34,12 +34,12 @@ function Panel({ const values = [ { - icon: html``, + icon: "type", value: "word counter", }, { // prettier-ignore - icon: html` + $icon: html` diff --git a/src/core/menu/islands/Input.mjs b/src/core/menu/islands/Input.mjs index f1d9570..4967645 100644 --- a/src/core/menu/islands/Input.mjs +++ b/src/core/menu/islands/Input.mjs @@ -11,8 +11,8 @@ const updateHotkey = (event) => { const alias = modifier[0].toUpperCase() + modifier.slice(1, -3); keys.push(alias); } - // retain tab for keyboard navigation of menu - if (event.key === "Tab" && !keys.length) { + // retain keyboard navigation of menu + if (["Tab", "Escape"].includes(event.key) && !keys.length) { return; } else event.preventDefault(); if (!keys.length && ["Backspace", "Delete"].includes(event.key)) { diff --git a/src/core/menu/islands/Select.mjs b/src/core/menu/islands/Select.mjs index 962a4f7..a50423a 100644 --- a/src/core/menu/islands/Select.mjs +++ b/src/core/menu/islands/Select.mjs @@ -6,7 +6,7 @@ import { Popup } from "./Popup.mjs"; -function Option({ icon = "", value = "", _get, _set }) { +function Option({ $icon = "", value = "", _get, _set }) { const { html, useState } = globalThis.__enhancerApi, $selected = html``, $option = html` event.target.focus()} onclick=${() => _set?.(value)} onkeydown=${(event) => { - if (event.key === "Enter") _set?.(value); + if (["Enter", " "].includes(event.key)) _set?.(value); }} > - ${icon}${value} + ${$icon}${value} `; useState(["rerender"], async () => { @@ -56,23 +57,52 @@ function Select({ duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]" >`; + const options = values.map((opt) => { + if (typeof opt === "string") opt = { value: opt }; + if (!(opt?.$icon instanceof Element)) { + if (opt?.icon && typeof opt.icon === "string") { + opt.$icon = html``; + } else delete opt.$icon; + } + opt.$option = html`<${Option} ...${{ ...opt, _get, _set }} />`; + opt.$selection = html` + + ${opt.value}${opt.$icon?.cloneNode(true) ?? ""} + `; + return opt; + }), + getSelected = async () => { + const value = (await _get?.()) ?? $select.innerText, + option = options.find((opt) => opt.value === value); + if (!option) _set(options[0].value); + return option || options[0]; + }, + onKeydown = (event) => { + const intercept = () => { + event.preventDefault(); + event.stopPropagation(); + }; + if (event.key === "Escape") { + intercept(setState({ rerender: true })); + } else if (!options.length) return; + // prettier-ignore + const $next = options.find(({ $option }) => $option === event.target) + ?.$option.nextElementSibling ?? options.at(0).$option, + $prev = options.find(({ $option }) => $option === event.target) + ?.$option.previousElementSibling ?? options.at(-1).$option; + // overflow to opposite end of list from dir of travel + if (event.key === "ArrowUp") intercept($prev.focus()); + if (event.key === "ArrowDown") intercept($next.focus()); + // re-enable natural tab behaviour in notion interface + if (event.key === "Tab") event.stopPropagation(); + }; + let _initialValue; - values = values.map((value) => { - value = typeof value === "string" ? { value } : value; - if (typeof value.icon === "string" && value.icon) { - value.icon = html``; - } else value.icon ??= ""; - value.value ??= ""; - return value; - }); useState(["rerender"], async () => { - const value = (await _get?.()) ?? ($select.innerText || values[0].value), - icon = values.find((v) => v.value === value)?.icon; + if (!options.length) return; + const { value, $selection } = await getSelected(); $select.innerHTML = ""; - // swap icon/value order for correct display when dir="rtl" - $select.append(html` - ${value}${icon?.cloneNode?.(true) || ""} - `); + $select.append($selection); if (_requireReload) { _initialValue ??= value; if (value !== _initialValue) setState({ databaseUpdated: true }); @@ -81,11 +111,13 @@ function Select({ extendProps(props, { class: "notion-enhancer--menu-select relative" }); return html` - ${$select} - <${Popup} + ${$select}<${Popup} + tabindex="0" trigger=${$select} mode=${popupMode} + onopen=${() => document.addEventListener("keydown", onKeydown, true)} onbeforeclose=${() => { + document.removeEventListener("keydown", onKeydown, true); $select.style.width = `${$select.offsetWidth}px`; $select.style.background = "transparent"; }} @@ -93,9 +125,7 @@ function Select({ $select.style.width = ""; $select.style.background = ""; }} - >${values.map((value) => { - return html`<${Option} ...${{ ...value, _get, _set }} />`; - })} + >${options.map(({ $option }) => $option)} /> { }); addKeyListener("Escape", () => { const [popupOpen] = useState(["popupOpen"]); - if (!popupOpen) { + if (document.activeElement?.tagName === "INPUT") { + document.activeElement.blur(); + } else if (!popupOpen) { const msg = { channel: "notion-enhancer", action: "close-menu" }; parent?.postMessage(msg, "*"); } else setState({ rerender: true });