fix(menu): keyboard triggering of inputs and switches via space/enter, don't trap tab in hotkey input

This commit is contained in:
dragonwocky 2023-01-24 00:15:10 +11:00
parent c3317bd9ec
commit 50a23f17c5
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
6 changed files with 59 additions and 27 deletions

View File

@ -25,7 +25,7 @@ function Button({ icon, variant, tagName, ...props }, ...children) {
hover:bg-[color:var(--theme--bg-hover)]` hover:bg-[color:var(--theme--bg-hover)]`
}`, }`,
}); });
return html`<${tagName ?? "button"} ...${props}> return html`<${tagName ?? "button"} tabindex="0" ...${props}>
${icon ${icon
? html`<i ? html`<i
class="i-${icon} class="i-${icon}

View File

@ -25,6 +25,12 @@ function Checkbox({ _get, _set, ...props }) {
return html`<label return html`<label
tabindex="0" tabindex="0"
class="notion-enhancer--menu-checkbox cursor-pointer" class="notion-enhancer--menu-checkbox cursor-pointer"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
> >
${$input} ${$input}
<div class="flex items-center h-[16px] transition duration-[200ms]"> <div class="flex items-center h-[16px] transition duration-[200ms]">

View File

@ -7,13 +7,16 @@
import { extendProps, useState } from "../state.mjs"; import { extendProps, useState } from "../state.mjs";
const updateHotkey = (event) => { const updateHotkey = (event) => {
event.preventDefault();
const keys = []; const keys = [];
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) { for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
if (!event[modifier]) continue; if (!event[modifier]) continue;
const alias = modifier[0].toUpperCase() + modifier.slice(1, -3); const alias = modifier[0].toUpperCase() + modifier.slice(1, -3);
keys.push(alias); keys.push(alias);
} }
// retain tab for keyboard navigation of menu
if (event.key === "Tab" && !keys.length) {
return;
} else event.preventDefault();
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) { if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
event.target.value = ""; event.target.value = "";
} else if (event.key) { } else if (event.key) {
@ -146,6 +149,12 @@ function Input({
h-[28px] px-[8px] bg-[color:var(--theme--bg-secondary)] h-[28px] px-[8px] bg-[color:var(--theme--bg-secondary)]
text-([14px] [color:var(--theme--fg-secondary)]) rounded-[4px] text-([14px] [color:var(--theme--fg-secondary)]) rounded-[4px]
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]" transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
>${$input}${$icon.children[0]}${$filename} >${$input}${$icon.children[0]}${$filename}
</label> </label>
${$clear} ${$clear}

View File

@ -50,7 +50,10 @@ function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
extendProps(trigger, { extendProps(trigger, {
onclick: $popup.show, onclick: $popup.show,
onkeydown(event) { onkeydown(event) {
if (event.key === "Enter") $popup.show(); if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$popup.show();
}
}, },
}); });
} }

View File

@ -11,7 +11,8 @@ function Toggle({ _get, _set, ...props }) {
$input = html`<input $input = html`<input
type="checkbox" type="checkbox"
class="hidden checked:sibling:children:( class="hidden checked:sibling:children:(
bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])" bg-[color:var(--theme--accent-primary)]
after:translate-x-[12px])"
...${props} ...${props}
/>`; />`;
extendProps($input, { onchange: () => _set?.($input.checked) }); extendProps($input, { onchange: () => _set?.($input.checked) });
@ -25,6 +26,12 @@ function Toggle({ _get, _set, ...props }) {
tabindex="0" tabindex="0"
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer class="w-[30px] h-[18px] rounded-[44px] cursor-pointer
transition duration-200 bg-[color:var(--theme--bg-hover)]" transition duration-200 bg-[color:var(--theme--bg-hover)]"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
> >
<div <div
class="w-full h-full rounded-[44px] text-[12px] class="w-full h-full rounded-[44px] text-[12px]

View File

@ -39,14 +39,14 @@ function Profile({ id }) {
class="py-[2px] px-[4px] rounded-[3px] class="py-[2px] px-[4px] rounded-[3px]
bg-[color:var(--theme--bg-hover)]" bg-[color:var(--theme--bg-hover)]"
></span>`, ></span>`,
$success = html`<${Popup} $uploadSuccess = html`<${Popup}
onopen=${async () => ($successName.innerText = await getName())} onopen=${async () => ($successName.innerText = await getName())}
> >
<p class="py-[2px] px-[8px] text-[14px]"> <p class="py-[2px] px-[8px] text-[14px]">
The profile ${$successName} has been updated successfully. The profile ${$successName} has been updated successfully.
</p> </p>
<//>`, <//>`,
$error = html`<${Popup}> $uploadError = html`<${Popup}>
<p <p
class="py-[2px] px-[8px] text-[14px] class="py-[2px] px-[8px] text-[14px]
text-[color:var(--theme--accent-secondary)]" text-[color:var(--theme--accent-secondary)]"
@ -60,16 +60,13 @@ function Profile({ id }) {
reader.onload = async (progress) => { reader.onload = async (progress) => {
const res = progress.currentTarget.result; const res = progress.currentTarget.result;
try { try {
await profile.import({ await profile.import(JSON.parse(res));
...JSON.parse(res),
profileName: await getName(),
});
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
$success.show(); $uploadSuccess.show();
setTimeout(() => $success.hide(), 2000); setTimeout(() => $uploadSuccess.hide(), 2000);
} catch (err) { } catch (err) {
$error.show(); $uploadError.show();
setTimeout(() => $error.hide(), 2000); setTimeout(() => $uploadError.hide(), 2000);
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@ -94,7 +91,14 @@ function Profile({ id }) {
$a.click(); $a.click();
$a.remove(); $a.remove();
}, },
deleteProfile = async () => { $uploadInput = html`<input
type="file"
class="hidden"
accept=".json"
onchange=${uploadProfile}
/>`;
const deleteProfile = async () => {
let profileIds = await db.get("profileIds"); let profileIds = await db.get("profileIds");
if (!profileIds?.length) profileIds = ["default"]; if (!profileIds?.length) profileIds = ["default"];
// clear profile data // clear profile data
@ -108,9 +112,8 @@ function Profile({ id }) {
await db.remove("activeProfile"); await db.remove("activeProfile");
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
} else setState({ rerender: true }); } else setState({ rerender: true });
}; },
$delete = html`<button
const $delete = html`<button
class="h-[14px] transition duration-[20ms] class="h-[14px] transition duration-[20ms]
text-[color:var(--theme--fg-secondary)] text-[color:var(--theme--fg-secondary)]
hover:text-[color:var(--theme--fg-primary)]" hover:text-[color:var(--theme--fg-primary)]"
@ -138,7 +141,7 @@ function Profile({ id }) {
<${Button} <${Button}
tabindex="0" tabindex="0"
class="justify-center" class="justify-center"
onclick=${() => $confirm.close()} onclick=${() => $confirm.hide()}
> >
Cancel Cancel
<//> <//>
@ -151,14 +154,18 @@ function Profile({ id }) {
onchange=${(event) => (event.target.checked = true)} onchange=${(event) => (event.target.checked = true)}
/> />
<${Input} icon="file-cog" ...${{ _get: getName, _set: setName }} /> <${Input} icon="file-cog" ...${{ _get: getName, _set: setName }} />
<${Button} variant="sm" icon="import" class="relative" tagName="label"> <${Button}
<input icon="import"
type="file" variant="sm"
class="hidden" tagName="label"
accept=".json" class="relative"
onchange=${uploadProfile} onkeydown=${(event) => {
/> if ([" ", "Enter"].includes(event.key)) {
Import ${$success} ${$error} event.preventDefault();
$uploadInput.click();
}
}}
>${$uploadInput} Import ${$uploadSuccess}${$uploadError}
<//> <//>
<${Button} variant="sm" icon="upload" onclick=${downloadProfile}>Export<//> <${Button} variant="sm" icon="upload" onclick=${downloadProfile}>Export<//>
<div class="relative flex">${$delete}${$confirm}</div> <div class="relative flex">${$delete}${$confirm}</div>