mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 08:11:03 +00:00
d06168d94a
After a tap, WebKit and Chrome keep :hover on what was tapped and send a
made-up mouseover, so buttons and rows stayed tinted until the next tap.
While a finger drives the app <html> carries data-touch-input: a PostCSS
step guards every :hover rule with it (no specificity change), and the
made-up mouseover and mouseout stop before any handler. A trackpad or mouse
clears the mark and gets hover back; a browser never sets it.
A control without a press of its own dims the moment a finger lands on it,
after a 60 ms wait so a scroll does not flash rows, and only the innermost
pressed control. A focus: ring no longer stays round a tapped button.
Fields get a fitting return key ("done", "go" in a form, "search" on the
search fields), and addresses, codes and numbers are no longer capitalised
or corrected. Ticking off packing items and tasks, a picker chip, a select
option, a picked date and dragging a plan row now give the haptics a native
list gives.
Confirmations ask through the system where the binary has the new
@capacitor/action-sheet and @capacitor/dialog plugins: a sheet with the
action in red when it destroys something, an alert otherwise, also for
"discard changes" in a sheet. An older app, a browser and a question with a
list under it keep the HTML dialog, as does a system that could not ask.
iOS no longer lifts a page preview on a long press on a link.
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
/**
|
|
* The keyboard a field gets in the app: what its return key says, and whether
|
|
* it capitalises and corrects what is typed.
|
|
*
|
|
* Left alone, every field in a web view gets the return key of a text editor
|
|
* and the autocorrect of a letter, so an email address starts with a capital,
|
|
* a booking code is "corrected" into a word, and Enter on a search field reads
|
|
* "return". Before a field is focused (on the touch that focuses it, or on the
|
|
* focus itself when code moves it there) this fills in what the field does not
|
|
* say for itself:
|
|
*
|
|
* - enterkeyhint: "go" in a form, which Enter submits, "done" anywhere else.
|
|
* Search fields say "search" in their own markup.
|
|
* - autocapitalize, autocorrect and spellcheck off for addresses, codes,
|
|
* numbers and names a machine reads (email, URL, password, tel, number;
|
|
* autocomplete such as username or one-time-code; a name or id such as
|
|
* "code", "token" or "host").
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
const SKIP_TYPES = new Set([
|
|
'button',
|
|
'checkbox',
|
|
'color',
|
|
'date',
|
|
'datetime-local',
|
|
'file',
|
|
'hidden',
|
|
'image',
|
|
'month',
|
|
'radio',
|
|
'range',
|
|
'reset',
|
|
'submit',
|
|
'time',
|
|
'week',
|
|
]);
|
|
const MACHINE_TYPES = new Set(['email', 'url', 'password', 'tel', 'number']);
|
|
const MACHINE_MODES = new Set(['email', 'url', 'numeric', 'decimal', 'tel']);
|
|
const MACHINE_AUTOCOMPLETE =
|
|
/\b(username|email|url|one-time-code|current-password|new-password|tel|postal-code|cc-[a-z-]+)\b/;
|
|
const MACHINE_WORDS = new Set([
|
|
'code',
|
|
'token',
|
|
'key',
|
|
'apikey',
|
|
'secret',
|
|
'host',
|
|
'hostname',
|
|
'url',
|
|
'uri',
|
|
'domain',
|
|
'slug',
|
|
'username',
|
|
'email',
|
|
'mail',
|
|
'iban',
|
|
'bic',
|
|
'pin',
|
|
'otp',
|
|
'totp',
|
|
'ref',
|
|
'reference',
|
|
'server',
|
|
'port',
|
|
'ip',
|
|
]);
|
|
|
|
/** The words of an id or name: "bookingCode", "booking_code" and "booking-code" all give booking, code. */
|
|
function words(...values: (string | null)[]): string[] {
|
|
return values
|
|
.filter((v): v is string => !!v)
|
|
.flatMap((v) =>
|
|
v
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
.toLowerCase()
|
|
.split(/[^a-z0-9]+/)
|
|
)
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function readByMachine(el: HTMLInputElement): boolean {
|
|
if (MACHINE_TYPES.has(el.type)) return true;
|
|
if (MACHINE_MODES.has((el.getAttribute('inputmode') ?? '').toLowerCase())) return true;
|
|
if (MACHINE_AUTOCOMPLETE.test((el.getAttribute('autocomplete') ?? '').toLowerCase())) return true;
|
|
return words(el.name, el.id).some((w) => MACHINE_WORDS.has(w));
|
|
}
|
|
|
|
function setIfAbsent(el: Element, name: string, value: string) {
|
|
if (!el.hasAttribute(name)) el.setAttribute(name, value);
|
|
}
|
|
|
|
export function prepareField(target: EventTarget | null): void {
|
|
if (!(target instanceof HTMLInputElement) || SKIP_TYPES.has(target.type) || target.readOnly) return;
|
|
const el = target;
|
|
const search = el.type === 'search' || el.getAttribute('role') === 'searchbox';
|
|
setIfAbsent(el, 'enterkeyhint', search ? 'search' : el.form ? 'go' : 'done');
|
|
if (search || readByMachine(el)) {
|
|
setIfAbsent(el, 'autocapitalize', 'none');
|
|
setIfAbsent(el, 'autocorrect', 'off');
|
|
setIfAbsent(el, 'spellcheck', 'false');
|
|
}
|
|
}
|
|
|
|
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);
|
|
return () => {
|
|
doc.removeEventListener('touchstart', onTouch, { capture: true });
|
|
doc.removeEventListener('focusin', onFocus, true);
|
|
};
|
|
}
|