Improve modals (#840)
* Adjust picker input to take up all remaining space * Improve modals and buttons * Remove confusing focus of scrollable result list in Firefox * Adjust css for dark themes, add background for text field in prompt Seperate accent color for text to increase contrast in dark theme Set css color-scheme to dark for dark theme * Fix buggy when entering very long text in picker * Prevent key events from propagating outside of modals * Always show focus on button * Add the keydown event listener directly to the mini editor * Do not refocus the mini editor when it loses focus and refactoring of the AlwaysShownModal. * Fix reference to button and mini editor focus in chrome * Fix selected option index capping in filter when using page downpull/854/head
parent
27ef256674
commit
cb6ee137f2
|
@ -1,6 +1,7 @@
|
|||
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { MiniEditor } from "./mini_editor.tsx";
|
||||
import { ComponentChildren, Ref } from "preact";
|
||||
|
||||
export function Prompt({
|
||||
message,
|
||||
|
@ -19,7 +20,11 @@ export function Prompt({
|
|||
}) {
|
||||
const [text, setText] = useState(defaultValue || "");
|
||||
const returnEl = (
|
||||
<div className="sb-modal-box">
|
||||
<AlwaysShownModal
|
||||
onCancel={() => {
|
||||
callback();
|
||||
}}
|
||||
>
|
||||
<div className="sb-prompt">
|
||||
<label>{message}</label>
|
||||
<MiniEditor
|
||||
|
@ -40,22 +45,25 @@ export function Prompt({
|
|||
setText(text);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
<div className="sb-prompt-buttons">
|
||||
<Button
|
||||
primary={true}
|
||||
onActivate={() => {
|
||||
callback(text);
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
</Button>
|
||||
<Button
|
||||
onActivate={() => {
|
||||
callback();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AlwaysShownModal>
|
||||
);
|
||||
|
||||
return returnEl;
|
||||
|
@ -73,49 +81,98 @@ export function Confirm({
|
|||
okButtonRef.current?.focus();
|
||||
});
|
||||
const returnEl = (
|
||||
<div className="sb-modal-wrapper">
|
||||
<div className="sb-modal-box">
|
||||
<div
|
||||
className="sb-prompt"
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
callback(true);
|
||||
break;
|
||||
case "Escape":
|
||||
<AlwaysShownModal
|
||||
onCancel={() => {
|
||||
callback(false);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="sb-prompt">
|
||||
<label>{message}</label>
|
||||
<div>
|
||||
<button
|
||||
ref={okButtonRef}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
<div className="sb-prompt-buttons">
|
||||
<Button
|
||||
buttonRef={okButtonRef}
|
||||
primary={true}
|
||||
onActivate={() => {
|
||||
callback(true);
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
</Button>
|
||||
<Button
|
||||
onActivate={() => {
|
||||
callback(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AlwaysShownModal>
|
||||
);
|
||||
|
||||
return returnEl;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
primary,
|
||||
onActivate,
|
||||
buttonRef,
|
||||
}: {
|
||||
children: ComponentChildren;
|
||||
primary?: boolean;
|
||||
onActivate: () => void;
|
||||
buttonRef?: Ref<HTMLButtonElement>;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={primary ? "sb-button-primary" : "sb-button"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onActivate();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onActivate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlwaysShownModal({
|
||||
children,
|
||||
onCancel,
|
||||
}: {
|
||||
children: ComponentChildren;
|
||||
onCancel?: () => void;
|
||||
}) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
dialogRef.current?.showModal();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
className="sb-modal-box"
|
||||
// @ts-ignore
|
||||
onCancel={(e: Event) => {
|
||||
e.preventDefault();
|
||||
onCancel?.();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
ref={dialogRef}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { FilterOption } from "$lib/web.ts";
|
|||
import { MiniEditor } from "./mini_editor.tsx";
|
||||
import { fuzzySearchAndSort } from "../fuse_search.ts";
|
||||
import { deepEqual } from "../../plug-api/lib/json.ts";
|
||||
import { AlwaysShownModal } from "./basic_modals.tsx";
|
||||
|
||||
export function FilterList({
|
||||
placeholder,
|
||||
|
@ -93,7 +94,11 @@ export function FilterList({
|
|||
}, []);
|
||||
|
||||
const returnEl = (
|
||||
<div className="sb-modal-box">
|
||||
<AlwaysShownModal
|
||||
onCancel={() => {
|
||||
onSelect(undefined);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sb-header"
|
||||
onClick={(e) => {
|
||||
|
@ -143,7 +148,9 @@ export function FilterList({
|
|||
setSelectionOption(Math.max(0, selectedOption - 5));
|
||||
return true;
|
||||
case "PageDown":
|
||||
setSelectionOption(Math.max(0, selectedOption + 5));
|
||||
setSelectionOption(
|
||||
Math.min(matchingOptions.length - 1, selectedOption + 5),
|
||||
);
|
||||
return true;
|
||||
case "Home":
|
||||
setSelectionOption(0);
|
||||
|
@ -170,7 +177,7 @@ export function FilterList({
|
|||
dangerouslySetInnerHTML={{ __html: helpText }}
|
||||
>
|
||||
</div>
|
||||
<div className="sb-result-list">
|
||||
<div className="sb-result-list" tabIndex={-1}>
|
||||
{matchingOptions && matchingOptions.length > 0
|
||||
? matchingOptions.map((option, idx) => (
|
||||
<div
|
||||
|
@ -203,7 +210,7 @@ export function FilterList({
|
|||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</AlwaysShownModal>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -64,19 +64,24 @@ export function MiniEditor(
|
|||
const callbacksRef = useRef<MiniEditorEvents>();
|
||||
|
||||
useEffect(() => {
|
||||
if (editorDiv.current) {
|
||||
const currentEditorDiv = editorDiv.current;
|
||||
if (currentEditorDiv) {
|
||||
// console.log("Creating editor view");
|
||||
const editorView = new EditorView({
|
||||
state: buildEditorState(),
|
||||
parent: editorDiv.current!,
|
||||
parent: currentEditorDiv,
|
||||
});
|
||||
editorViewRef.current = editorView;
|
||||
|
||||
const focusEditorView = editorView.focus.bind(editorView);
|
||||
currentEditorDiv.addEventListener("focusin", focusEditorView);
|
||||
|
||||
if (focus) {
|
||||
editorView.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
currentEditorDiv.removeEventListener("focusin", focusEditorView);
|
||||
if (editorViewRef.current) {
|
||||
editorViewRef.current.destroy();
|
||||
}
|
||||
|
@ -107,15 +112,12 @@ export function MiniEditor(
|
|||
}
|
||||
}, [text, vimMode]);
|
||||
|
||||
useEffect(() => {
|
||||
// So, for some reason, CM doesn't propagate the keydown event, therefore we'll capture it here
|
||||
// And check if it's the same editor element
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const parent = (e.target as any).parentElement.parentElement;
|
||||
if (parent !== editorViewRef.current?.dom) {
|
||||
// Different editor element
|
||||
return;
|
||||
}
|
||||
let onBlurred = false, onEntered = false;
|
||||
|
||||
return (
|
||||
<div
|
||||
class="sb-mini-editor"
|
||||
onKeyDown={(e) => {
|
||||
let stopPropagation = false;
|
||||
if (callbacksRef.current!.onKeyDown) {
|
||||
stopPropagation = callbacksRef.current!.onKeyDown(
|
||||
|
@ -127,17 +129,10 @@ export function MiniEditor(
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let onBlurred = false, onEntered = false;
|
||||
|
||||
return <div class="sb-mini-editor" ref={editorDiv} />;
|
||||
}}
|
||||
ref={editorDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
function buildEditorState() {
|
||||
// When vim mode is active, we need for CM to have created the new state
|
||||
|
@ -262,12 +257,6 @@ export function MiniEditor(
|
|||
// Reset the state
|
||||
view.setState(buildEditorState());
|
||||
});
|
||||
} else if (focus) {
|
||||
// console.log("BLURRING WHILE KEEPING FOCUSE");
|
||||
// Automatically refocus blurred
|
||||
if (editorViewRef.current) {
|
||||
editorViewRef.current.focus();
|
||||
}
|
||||
}
|
||||
// Event may occur again in 500ms
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -80,6 +80,39 @@
|
|||
color: var(--action-button-hover-color);
|
||||
}
|
||||
|
||||
.sb-button, .sb-button-primary {
|
||||
--color: var(--button-color);
|
||||
--background-color: var(--button-background-color);
|
||||
--hover-background-color: var(--button-hover-background-color);
|
||||
--border-color: var(--button-border-color);
|
||||
|
||||
&.sb-button-primary {
|
||||
--color: var(--primary-button-color);
|
||||
--background-color: var(--primary-button-background-color);
|
||||
--hover-background-color: var(--primary-button-hover-background-color);
|
||||
--border-color: var(--primary-button-border-color);
|
||||
}
|
||||
|
||||
background-color: var(--background-color);
|
||||
color: var(--color);
|
||||
|
||||
box-shadow: 0 0 0.2em rgba(0, 0, 0, 0.05);
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.2em 0.5em;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-background-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--color);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal boxes */
|
||||
.sb-modal-box {
|
||||
color: var(--modal-color);
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
.sb-modal-box {
|
||||
position: absolute;
|
||||
// At the toppest of the toppest
|
||||
z-index: 1000;
|
||||
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 60px;
|
||||
padding: 0;
|
||||
width: 700px;
|
||||
max-width: 90%;
|
||||
|
||||
|
@ -27,6 +22,11 @@
|
|||
label {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.sb-mini-editor {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-prompt {
|
||||
|
@ -37,11 +37,20 @@
|
|||
}
|
||||
|
||||
.sb-mini-editor {
|
||||
background-color: var(--text-field-background-color);
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
padding: 0.2em 0.5em;
|
||||
border: 0;
|
||||
border-radius: 0.5em;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sb-prompt-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-help-text {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
html {
|
||||
--ui-accent-color: #464cfc;
|
||||
--ui-accent-text-color: var(--ui-accent-color);
|
||||
--ui-accent-contrast-color: #eee;
|
||||
--highlight-color: rgba(255, 255, 0, 0.5);
|
||||
--link-color: #0330cb;
|
||||
--link-missing-color: #9e4705;
|
||||
|
@ -29,11 +31,11 @@ html {
|
|||
--modal-color: inherit;
|
||||
--modal-background-color: #fff;
|
||||
--modal-border-color: rgb(108, 108, 108);
|
||||
--modal-header-label-color: var(--ui-accent-color);
|
||||
--modal-header-label-color: var(--ui-accent-text-color);
|
||||
--modal-help-background-color: #eee;
|
||||
--modal-help-color: #555;
|
||||
--modal-selected-option-background-color: var(--ui-accent-color);
|
||||
--modal-selected-option-color: #eee;
|
||||
--modal-selected-option-color: var(--ui-accent-contrast-color);
|
||||
--modal-hint-background-color: #212476;
|
||||
--modal-hint-color: #eee;
|
||||
--modal-description-color: #aaa;
|
||||
|
@ -43,6 +45,17 @@ html {
|
|||
--notification-info-background-color: rgb(187, 221, 247);
|
||||
--notification-error-background-color: rgb(255, 84, 84);
|
||||
|
||||
--button-background-color: #eee;
|
||||
--button-hover-background-color: inherit;
|
||||
--button-color: black;
|
||||
--button-border-color: #6c6c6c;
|
||||
--primary-button-background-color: var(--ui-accent-color);
|
||||
--primary-button-hover-background-color: color-mix(in srgb, var(--ui-accent-color), black 35%);
|
||||
--primary-button-color: var(--ui-accent-contrast-color);
|
||||
--primary-button-border-color: transparent;
|
||||
|
||||
--text-field-background-color: var(--button-background-color);
|
||||
|
||||
--action-button-background-color: transparent;
|
||||
--action-button-color: #292929;
|
||||
--action-button-hover-color: #0772be;
|
||||
|
@ -116,7 +129,11 @@ html {
|
|||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--ui-accent-color: #464cfc;
|
||||
--ui-accent-text-color: var(--ui-accent-color);
|
||||
--ui-accent-text-color: color-mix(in srgb, var(--ui-accent-color), white 50%);
|
||||
--highlight-color: rgba(255, 255, 0, 0.5);
|
||||
--link-color: #7e99fc;
|
||||
--link-missing-color: #9e4705;
|
||||
|
@ -146,7 +163,7 @@ html[data-theme="dark"] {
|
|||
--modal-color: #ccc;
|
||||
--modal-background-color: #262626;
|
||||
--modal-border-color: #6c6c6c;
|
||||
--modal-header-label-color: var(--ui-accent-color);
|
||||
--modal-header-label-color: var(--ui-accent-text-color);
|
||||
--modal-help-background-color: #333;
|
||||
--modal-help-color: #ccc;
|
||||
--modal-selected-option-background-color: var(--ui-accent-color);
|
||||
|
@ -160,6 +177,17 @@ html[data-theme="dark"] {
|
|||
--notification-info-background-color: #1b76bb;
|
||||
--notification-error-background-color: #a32121;
|
||||
|
||||
--button-background-color: #555;
|
||||
--button-hover-background-color: #777;
|
||||
--button-color: white;
|
||||
--button-border-color: #666;
|
||||
--primary-button-background-color: var(--ui-accent-color);
|
||||
--primary-button-hover-background-color: color-mix(in srgb, var(--ui-accent-color), black 35%);
|
||||
--primary-button-color: var(--ui-accent-contrast-color);
|
||||
--primary-button-border-color: transparent;
|
||||
|
||||
--text-field-background-color: var(--button-background-color);
|
||||
|
||||
--action-button-background-color: transparent;
|
||||
--action-button-color: #adadad;
|
||||
--action-button-hover-color: #37a1ed;
|
||||
|
|
Loading…
Reference in New Issue