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 down
pull/854/head
Daniel Michel 2024-04-20 16:22:02 +02:00 committed by GitHub
parent 27ef256674
commit cb6ee137f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 233 additions and 110 deletions

View File

@ -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={() => {
callback(text);
}}
>
Ok
</button>
<button
onClick={() => {
callback();
}}
>
Cancel
</button>
<div className="sb-prompt-buttons">
<Button
primary={true}
onActivate={() => {
callback(text);
}}
>
Ok
</Button>
<Button
onActivate={() => {
callback();
}}
>
Cancel
</Button>
</div>
</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":
callback(false);
break;
}
}}
>
<label>{message}</label>
<div>
<button
ref={okButtonRef}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
callback(true);
}}
>
Ok
</button>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
callback(false);
}}
>
Cancel
</button>
</div>
<AlwaysShownModal
onCancel={() => {
callback(false);
}}
>
<div className="sb-prompt">
<label>{message}</label>
<div className="sb-prompt-buttons">
<Button
buttonRef={okButtonRef}
primary={true}
onActivate={() => {
callback(true);
}}
>
Ok
</Button>
<Button
onActivate={() => {
callback(false);
}}
>
Cancel
</Button>
</div>
</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>
);
}

View File

@ -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(() => {

View File

@ -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,37 +112,27 @@ 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 stopPropagation = false;
if (callbacksRef.current!.onKeyDown) {
stopPropagation = callbacksRef.current!.onKeyDown(
editorViewRef.current!,
e,
);
}
if (stopPropagation) {
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} />;
return (
<div
class="sb-mini-editor"
onKeyDown={(e) => {
let stopPropagation = false;
if (callbacksRef.current!.onKeyDown) {
stopPropagation = callbacksRef.current!.onKeyDown(
editorViewRef.current!,
e,
);
}
if (stopPropagation) {
e.preventDefault();
e.stopPropagation();
}
}}
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(() => {

View File

@ -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);

View File

@ -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 {

View File

@ -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;