今天要來講的是有關於有數值格式化的場景中,React input 光標的一些異常的表現和對應的處理辦法。故事要從一個 issue 提及,有用戶反映在使用 NumberField 組件輸入時安卓下會出現光標位置異常,致使連續輸入會達不到指望的結果。具體表現是什麼樣的呢?react
圖1 安卓下不指望的輸入行爲
能夠看到,在安卓手機下每次格式化發生的時候,原本應該一直在最後的光標會錯格一位,致使連續輸入出現問題。而這個問題在 PC Chrome 和 iOS 上都沒有出現,因而能夠斷定是一個兼容性問題。但這個兼容性問題是如何產生的呢?git
分析一下格式化的話的過程,如上面的狀況,輸入 18758 時,由於要作針對卡號的格式化,因此會將原有的值轉變爲 "1875 8",從字符串長度上來看,從 5 位變成了 6 位,那麼若是此時光標位置沒有在值變化時跳到最後一位,則會停留在空格處,看起來就好像錯格了一位,連續輸入時就會有問題。github
單從輸入框的光標變化行爲來看,這好像也不算是一種異常的變化,只是不響應值的變化跳到尾部而已。但引伸出來的問題是爲何在 iOS 和 PC Chrome 下又會跳動到尾部呢。算法
圖2: 相同的代碼在 PC Chrome 下表現與安卓不一樣。
因而去網上搜索,展轉在 React 的 github 中找到這樣一個 issue, Cursor jumps to end of controlled input。在這裏 React 的主要維護者之一的 @sophiebits(spicyj) 給出了一個比較確切的答案。瀏覽器
圖3 sophiebits 關於 React controlled input value 變化時光標行爲的解釋
原來由於 value 的變化具備很是大的不肯定性,所以 React 沒法使用一種可靠且通用的邏輯去保存光標的位置在一個合適的位置,所以 React 在受控模式下的從新渲染都會時光標移動到最後的位置。這個至少解釋了PC Chrome 和 iOS 下光標跳動到結尾的緣由,但安卓下爲何沒有表現出一樣的行爲到目前位置我尚未找到合理的解釋。異步
那有沒有辦法使安卓上的表現和 iOS 中一致呢?又是一陣翻閱和嘗試,最後發現若是將從新渲染的過程和 input 的 onChange 置於先後兩個 tick 中就可使安卓中 input 的表現和其餘平臺上表現一致,即表現爲光標在從新渲染時跳到最後,示意代碼以下。ui
import React from 'React'; class Demo extends React.Component { constructor(props) { super(props); this.state = { value: 'xxx', }; } handleChange(e) { const value = e.target.value; // 經過 setTimeout 異步 // 使 re-render 和 onChange 處於兩個 tick 中 setTimeout(() => { this.setState({ value, }); }); } render() { return ( <input value={this.state.value} onChange={(e) => { this.handleChange(e); }} /> ); } }
這樣終於使得表現的行爲在安卓和 iOS 上表現一致,而且正常輸入的狀況下表現得比較符合指望了,然而等等,這樣就能夠了嗎?從以前的 React issue 中得出的結論能夠看出,不管是如何的修改都會跳至 input 的結尾,這樣若是是從中間修改的話會變成什麼樣?this
圖4:中間編輯時又會出現問題
從上面的圖裏能夠看出,由於 React 不管何種修改都會將光標置尾,若是從中間進行修改,那麼表現地又會很不符合用戶預期,沒有辦法作到連續輸入。這回卻是兩端行爲保持一致,都是不指望的狀態。。spa
可是都不正常也有好處,不須要根據平臺去寫一些 ifelse,能夠統一地去作處理。從上面的討論中咱們能夠知道 React 沒有保存光標的位置是由於沒有一個通用而且可靠的算法去支撐這一行爲。這是由於 input 的變化多是增長空格作格式化,也多是過濾過些字符,也多是觸發某些條件直接變成了其餘字符等各類沒法預測的變化行爲。可是細化到數字格式化這一單一場景時,光標位置的保存邏輯就變得簡單和清晰的多了。3d
在用戶輸入的過程當中,只存在兩種狀況,在結尾中追加和在中間修改。在結尾追加的 case 中,例如 18758^ 時,因爲一直是在向後追加的狀態,咱們只要一直保持光標在最後便可(即默認狀態 1875 8^ ),在中間編輯的 case 下,光標並不處於結尾,如 187^5 8,此時若是在 7 後面追加了一個 8,那麼理想的圖標應該維持在 8 以後(即 1878^ 58),此時就應該保存光標的位置在上次 format 以前的狀態。
邏輯清楚了,接下來就是如何實現的問題了。那麼如何探測和修改光標位置呢?這就涉及了 input 中選區相關的屬性,咱們知道咱們能夠經過一些方式(如鼠標拖拽和長按屏幕等)在 input 中完成對一段話的選區,所以光標的位置實際上是由選區的開始點(selectionStart)和結束點(selectionEnd)決定的。那麼其實咱們就能夠經過讀取,儲存和設置這兩個屬性來達到咱們想要實現的目的,實例代碼以下。
class Demo extends React.Component { ... componentDidUpdate(prevProps) { const { value } = prevProps; const { inputSelection } = this; if (inputSelection) { // 在 didUpdate 時根據狀況恢復光標的位置 // 若是光標的位置小於值的長度,那麼能夠斷定屬於中間編輯的狀況 // 此時恢復光標的位置 if (inputSelection.start < this.formatValue(value).length) { const input = this.input; input.selectionStart = inputSelection.start; input.selectionEnd = inputSelection.end; this.inputSelection = null; } } } handleChange(e) { // 在 onChange 時記錄光標的位置 if (this.input) { this.inputSelection = { start: this.input.selectionStart, end: this.input.selectionEnd, }; } ... } render() { return ( <input ref={(c) => { this.input = c; }} value={this.state.value} onChange={(e) => { this.handleChange(e); }} /> ); } }
至此,咱們終於在追加和中間編輯的狀況下都實現了咱們想要的效果。這是一個比較小的技術點,可是因爲裏面涉及了一些 React 內部的處理邏輯及平臺差別性問題,排查和解決起來並非那麼容易,但願能夠給有相似問題的同窗在處理時有所啓發。
Mozilla/5.0 (Linux; U; Android 8.1.0; zh-CN; CLT-AL00 Build/HUAWEICLT-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/11.9.4.974 UWS/2.13.1.48 Mobile Safari/537.36
Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15F79
Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36