mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(native): the keyboard's done key closes the keyboard
A field outside a form gets enterkeyhint="done", but a web view only sends Enter and nothing listened, so the key did nothing. When no handler took the Enter (same text, still focused a moment later) the field lets go of the focus; a field that acts on Enter and clears itself keeps it. TestFlight feedback, 5.1.3.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { prepareField, prepareKeyboardFields } from './keyboardFields';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { closeOnDone, prepareField, prepareKeyboardFields } from './keyboardFields';
|
||||
|
||||
function field(html: string): HTMLInputElement {
|
||||
document.body.innerHTML = html;
|
||||
@@ -89,3 +89,53 @@ describe('prepareKeyboardFields', () => {
|
||||
expect(b.hasAttribute('enterkeyhint')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeOnDone', () => {
|
||||
const enter = (el: HTMLElement) => el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
|
||||
it('closes the keyboard when nothing took the Enter of a "done" field', () => {
|
||||
vi.useFakeTimers();
|
||||
const el = field('<input name="title" enterkeyhint="done">');
|
||||
el.focus();
|
||||
el.addEventListener('keydown', closeOnDone);
|
||||
enter(el);
|
||||
vi.runAllTimers();
|
||||
expect(document.activeElement).not.toBe(el);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps the keyboard for a field that acted on the Enter and cleared itself', () => {
|
||||
vi.useFakeTimers();
|
||||
const el = field('<input name="item" enterkeyhint="done">');
|
||||
el.value = 'Socks';
|
||||
el.focus();
|
||||
el.addEventListener('keydown', () => {
|
||||
el.value = '';
|
||||
});
|
||||
el.addEventListener('keydown', closeOnDone);
|
||||
enter(el);
|
||||
vi.runAllTimers();
|
||||
expect(document.activeElement).toBe(el);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('leaves "go" and "search" fields and handled Enters alone', () => {
|
||||
vi.useFakeTimers();
|
||||
for (const hint of ['go', 'search']) {
|
||||
const el = field(`<input enterkeyhint="${hint}">`);
|
||||
el.focus();
|
||||
el.addEventListener('keydown', closeOnDone);
|
||||
enter(el);
|
||||
vi.runAllTimers();
|
||||
expect(document.activeElement).toBe(el);
|
||||
}
|
||||
const handled = field('<input enterkeyhint="done">');
|
||||
handled.focus();
|
||||
handled.addEventListener('keydown', (e) => e.preventDefault());
|
||||
handled.addEventListener('keydown', closeOnDone);
|
||||
handled.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
||||
vi.runAllTimers();
|
||||
expect(document.activeElement).toBe(handled);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
*
|
||||
* An attribute the field sets itself always wins, and textareas, which take
|
||||
* Enter as a new line, keep the return key. Only the app calls this.
|
||||
*
|
||||
* The "done" key also has to close the keyboard: a web view only sends Enter,
|
||||
* and a field outside a form has nothing listening for it, so the key did
|
||||
* nothing. When no handler took the Enter (the field still has focus and the
|
||||
* same text a moment later) the field lets go of the focus. A field that acts
|
||||
* on Enter and clears itself, such as a list that adds the next item, keeps it.
|
||||
*/
|
||||
|
||||
const SKIP_TYPES = new Set([
|
||||
@@ -104,14 +110,29 @@ export function prepareField(target: EventTarget | null): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Enter on a "done" field that nothing acted on closes the keyboard. */
|
||||
export function closeOnDone(e: KeyboardEvent): void {
|
||||
const el = e.target;
|
||||
if (e.key !== 'Enter' || e.isComposing || e.defaultPrevented) return;
|
||||
if (!(el instanceof HTMLInputElement) || el.getAttribute('enterkeyhint') !== 'done') return;
|
||||
const before = el.value;
|
||||
// After every handler of this Enter, including ones React schedules.
|
||||
setTimeout(() => {
|
||||
if (el.ownerDocument.activeElement === el && el.value === before) el.blur();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function prepareKeyboardFields(doc: Document = document): () => void {
|
||||
// On the touch, before the focus: the keyboard is set up as the field takes it.
|
||||
const onTouch = (e: Event) => prepareField(e.target instanceof Element ? e.target.closest('input') : null);
|
||||
const onFocus = (e: FocusEvent) => prepareField(e.target);
|
||||
doc.addEventListener('touchstart', onTouch, { capture: true, passive: true });
|
||||
doc.addEventListener('focusin', onFocus, true);
|
||||
// Bubbling, so a field's own handler runs first and can take the Enter.
|
||||
doc.addEventListener('keydown', closeOnDone);
|
||||
return () => {
|
||||
doc.removeEventListener('touchstart', onTouch, { capture: true });
|
||||
doc.removeEventListener('focusin', onFocus, true);
|
||||
doc.removeEventListener('keydown', closeOnDone);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user