Move focus into HTML editor when shown (#1861)

* Move focus into input field, when input is shown

* Change trapFocusOut to move focus into available inputs

- This means that e.g. closing the HTML editor with focus in it will
  focus the visual editor in turn

* Prevent Control+A unselecting tag editor when no tags exist
This commit is contained in:
Henrik Giesel 2022-05-13 05:02:03 +02:00 committed by GitHub
parent de2cc20c59
commit 52438fe4c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 25 deletions

View File

@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import contextProperty from "../sveltelib/context-property";
export interface EditingInputAPI {
export interface FocusableInputAPI {
readonly name: string;
focusable: boolean;
/**
@ -22,6 +22,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
refocus(): void;
}
export interface EditingInputAPI extends FocusableInputAPI {
/**
* Check whether blurred target belongs to an editing input.
* The editing area can then restore focus to this input.
*
* @returns An editing input api that is associated with the event target.
*/
getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null>;
}
export interface EditingAreaAPI {
content: Writable<string>;
editingInputs: Writable<EditingInputAPI[]>;
@ -36,7 +46,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { setContext as svelteSetContext } from "svelte";
import { setContext as svelteSetContext, tick } from "svelte";
import { writable } from "svelte/store";
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
@ -116,12 +126,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
// prevents editor field being entirely deselected when
// closing active field
function trapFocusOnBlurOut(event: FocusEvent): void {
if (!event.relatedTarget && editingInputs.every((input) => !input.focusable)) {
// Prevents editor field being entirely deselected when
// closing active field.
async function trapFocusOnBlurOut(event: FocusEvent): Promise<void> {
if (event.relatedTarget) {
return;
}
event.preventDefault();
const oldInputElement = event.target;
await tick();
let focusableInput: FocusableInputAPI | null = null;
const focusableInputs = editingInputs.filter(
(input: EditingInputAPI): boolean => input.focusable,
);
if (oldInputElement) {
for (const input of focusableInputs) {
focusableInput = await input.getInputAPI(oldInputElement);
if (focusableInput) {
break;
}
}
}
if (focusableInput || (focusableInput = focusableInputs[0])) {
focusableInput.focus();
} else {
focusTrap.focus();
event.preventDefault();
}
}

View File

@ -324,8 +324,26 @@ the AddCards dialog) should be implemented in the user of this component.
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
<RichTextBadge bind:off={richTextsHidden[index]} />
<PlainTextBadge bind:off={plainTextsHidden[index]} />
<RichTextBadge
bind:off={richTextsHidden[index]}
on:toggle={() => {
richTextsHidden[index] = !richTextsHidden[index];
if (!richTextsHidden[index]) {
richTextInputs[index].api.refocus();
}
}}
/>
<PlainTextBadge
bind:off={plainTextsHidden[index]}
on:toggle={() => {
plainTextsHidden[index] = !plainTextsHidden[index];
if (!plainTextsHidden[index]) {
plainTextInputs[index].api.refocus();
}
}}
/>
<slot name="field-state" {field} {index} />
</svelte:fragment>

View File

@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
import Badge from "../components/Badge.svelte";
import * as tr from "../lib/ftl";
@ -13,13 +13,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const editorField = editorFieldContext.get();
const keyCombination = "Control+Shift+X";
const dispatch = createEventDispatcher();
export let off = false;
$: icon = off ? htmlOff : htmlOn;
function toggle() {
off = !off;
dispatch("toggle");
}
function shortcut(target: HTMLElement): () => void {

View File

@ -3,14 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Badge from "../components/Badge.svelte";
import * as tr from "../lib/ftl";
import { richTextOff, richTextOn } from "./icons";
export let off: boolean;
function toggle(): void {
off = !off;
const dispatch = createEventDispatcher();
function toggle() {
dispatch("toggle");
}
$: icon = off ? richTextOff : richTextOn;

View File

@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { registerPackage } from "../../lib/runtime-require";
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
import type { EditingInputAPI } from "../EditingArea.svelte";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
export interface PlainTextInputAPI extends EditingInputAPI {
name: "plain-text";
@ -39,6 +39,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import removeProhibitedTags from "./remove-prohibited";
import { storedToUndecorated, undecoratedToStored } from "./transform";
export let hidden: boolean;
const configuration = {
mode: htmlanki,
...baseOptions,
@ -46,7 +48,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
};
const { focusedInput } = noteEditorContext.get();
const { editingInputs, content } = editingAreaContext.get();
const code = writable($content);
@ -70,13 +71,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
moveCaretToEnd();
}
export let hidden = false;
function toggle(): boolean {
hidden = !hidden;
return hidden;
}
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
const editor = (await codeMirror.editor) as any;
if (target === editor.display.input.textarea) {
return api;
}
return null;
}
export const api: PlainTextInputAPI = {
name: "plain-text",
focus,
@ -84,6 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
moveCaretToEnd,
refocus,
toggle,
getInputAPI,
codeMirror,
};

View File

@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
import useInputHandler from "../../sveltelib/input-handler";
import { pageTheme } from "../../sveltelib/theme";
import type { EditingInputAPI } from "../EditingArea.svelte";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
import type CustomStyles from "./CustomStyles.svelte";
export interface RichTextInputAPI extends EditingInputAPI {
@ -92,6 +92,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return hidden;
}
const className = "rich-text-editable";
let richTextDiv: HTMLElement;
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
if (target === richTextDiv) {
return api;
}
return null;
}
export const api: RichTextInputAPI = {
name: "rich-text",
element: richTextPromise,
@ -99,6 +110,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
refocus,
focusable: !hidden,
toggle,
getInputAPI,
moveCaretToEnd,
preventResubscription,
inputHandler,
@ -155,7 +167,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:stylesDidLoad
>
<div
class="rich-text-editable"
bind:this={richTextDiv}
class={className}
class:hidden
class:night-mode={$pageTheme.isDark}
use:attachShadow

View File

@ -346,7 +346,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
function selectAllTags() {
tagTypes.forEach((tag) => (tag.selected = true));
for (const tag of tagTypes) {
tag.selected = true;
}
tagTypes = tagTypes;
}
@ -451,7 +454,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
splitTag(index, detail.start, detail.end)}
on:tagadd={() => insertTagKeepFocus(index)}
on:tagdelete={() => deleteTagAt(index)}
on:tagselectall={selectAllTags}
on:tagselectall={async () => {
if (tagTypes.length <= 1) {
// Noop if no other tags exist
return;
}
activeInput.blur();
// Ensure blur events are processed first
await tick();
selectAllTags();
}}
on:tagjoinprevious={() => joinWithPreviousTag(index)}
on:tagjoinnext={() => joinWithNextTag(index)}
on:tagmoveprevious={() => moveToPreviousTag(index)}

View File

@ -231,13 +231,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
name = last;
}
async function onSelectAll(event: KeyboardEvent) {
function onSelectAll(event: KeyboardEvent) {
if (name.length === 0) {
input.blur();
await tick(); // ensure blur events are processed before tagselectall
dispatch("tagselectall");
event.preventDefault();
event.stopPropagation();
dispatch("tagselectall");
}
}