InputEvent丢失inputType

2024-09-10

问题描述

业务场景

当input使用了自定义键盘,点击删除键时,无法正常删除输入框中的内容;而使用系统键盘时,表现正常。

card-input

复现环境

ios < 16.5

原因分析

经过调试,发现在ios部分机型上InputEvent的inputType为空字符串,判断键入删除操作的逻辑失效,导致无法删除。

20240910181250

Sail组件库有一个设计原则,就是尽量与系统原生行为保持一致。这里为了让input在使用了自定义键盘后,行为与使用系统键盘一致,按下删除键时,会手动创建一个InputEvent事件,并由input元素dispatch出去。这样对于调用者来说,不管是系统键盘,还是自定义键盘,在按下删除键时,如下代码都会有一致的行为。

input.addEventListener('input', (e: InputEvent) => {
  console.log(e.inputType); // deleteContentBackward
});

而系统键盘的backspace键能正常删除,但自定义键盘不能,两者的区别就是系统键盘的InputEvent是系统触发的,自定义键盘是由我们的代码new InputEvent并dispatch的,由此可见在部分机型上new InputEvent的表现与系统抛出的事件表现不一致,可能存在兼容性问题。

问题本质

ios < 16.5,通过new InputEvent('input', { inputType: 'deleteContentBackward' })手动创建的事件,监听后event.inputType为空字符串,而期望得到deleteContentBackward。经实测,不仅是deleteContentBackward,所有的inputType值都会变为空字符串。

20240911103020

最小复现步骤

演练场传送门

<template>
  <button @click="clicked">触发事件</button>
  <input ref="el" @input="onInput" />
</template>

<script lang="ts" setup>
const el = ref();

function clicked() {
  const event = new InputEvent('input', { inputType: 'deleteContentBackward' });
  el.value.dispatchEvent(event);
}

function onInput(e: InputEvent) {
  console.log(e.inputType); // ios < 16.5为空字符串,其它版本为'deleteContentBackward'
}
</script>

通过查阅MDN、caniuse、w3c文档可以看到inputType是InputEvent的标准属性,各主流浏览器在很早均实现了该接口,并无兼容性问题。

caniuse传送门

caniuse

W3C传送门

w3c

MDN传送门

MDN

实测表现

  • 系统键盘,所有系统下都能正常读取event.inputType
  • 手动dispatch的InputEvent事件,Android > 4.4.4(与caniuse描述一致),ios >= 16.5能正常读取到。
  • 手动dispatch的InputEvent事件,ios < 16.5获取到空字符串。

由此判断应该是ios系统的bug,苹果在16.5修复了此问题。

解决方案

通过文档可以发现InputEvent还有一个属性data,用来设置输入的内容,可以借助这个属性来传递inputType。

20240910173017

核心代码

以删除操作为例

// 创建事件
const event = new InputEvent('input', {
  inputType: 'deleteContentBackward',
  data: 'deleteContentBackward',
});

el.dispatchEvent(event);

// 监听事件
el.addEventListener('input', (e: InputEvent) => {
  if (isDeleteContentBackward(e)) {
    // 业务逻辑
  }
})

/**
 * 判断是否是删除事件
 */
const isDeleteContentBackward = ({ inputType, data }: InputEvent) => {
  // 如果inputType有值,则优先用inputType判断
  if (inputType === 'deleteContentBackward') {
    return true;
  }
  /**
   * 当inpuType为空时,通过e.data === 'deleteContentBackward'做二次兼容判断
   */
  if (!inputType && data === 'deleteContentBackward') {
    return true;
  }

  return false;
};