読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

iOSのMobile Safari上でのfocus()が妙な件を調べてみた

JavaScript mixi

こんにちは。ミクシィUX統括部にて、mixi Touchの開発を担当しております戸高です。
JavaScriptでの開発を行う事が多いのですが、iOSのブラウザ(Mobile Safari)のfocusの挙動について少しクセがあり、
調べてみましたので、お知らせいたします。

通常、<textarea>や<input type=”text”>等のフォーム要素に対して、フォーカスを与えたい場合、focus()メソッドを使用します。
iOSのブラウザ(以下、Mobile Safari)にて、以下のコードを実行してみます。
(分かりやすい様にjQueryを使用させて頂きました)
なお、検証端末にはiPhone4S iOS5.0(9A334)を使用しています。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv=”Content-Type” content=”text/html;charset=utf-8″ />
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, user-scalable=no” />
    <title>foucs() test</title>
    <script type=”text/javascript” src=”http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js”></script>
    <script type=”text/javascript” src=”test.js”></script>
  </head>
  <body>
    <input id=”testTextField1″ type=”text” value=”" /><br />
    <input id=”testButton” type=”button” value=”clickme” /><br />
  </body>
</html>

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testTextField1').focus();
});

ところがどうでしょう、このコードではバーチャルキーボードも起動されず、何も起こっていない様に見えます。
しかし、console.log()は出力されているのでfocusイベント自体はディスパッチされていて、ハンドラが実行されているようです。
何とも不思議な現象です。

さて、次に以下のコードを実行してみます。

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    var onClickHandler = function(){
        $('#testTextField1').focus();
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testButton').bind('click', onClickHandler);
});

ボタンをクリック(タップ)すると、focus()する様にしてみました。
ところが今度はフォームにカーソルが合い、バーチャルキーボードが出現しました。
勿論、console.logも出力されています。
これが最初のコードで期待されていた動作です。

では、何故clickイベントの場合でfocus()が正常な動作をしたのでしょうか。
まず手始めに上記のclick同様、各イベントのハンドラ内でfocus()をしてあげるとどうなるかという事をMobile Safariで調べてみました。
結果は以下の通り。

× window.onload
× document.ready
click
× change
× focus
× blur
× dblclick ★
× keydown ★
× keypress ★
mousedown ★
mouseup ★
× mouseout ★
mouseover ★
touchstart
touchend
touchmove

* touchcancelは特殊な場合なので割愛
** Touchデバイスでは通常使用されない様なイベントには★をつけてあります。

どうやら、このfocus()が正常に動作(カーソルが合い、バーチャルキーボードが出現)する場合は、
特定のイベントのディスパッチ時のみの様です。

そもそもMobile Safariも元々はマウスのある(mouseover等のイベント等がある)ブラウザの派生の可能性が高いという事を考え、
Safariの<input type=”text”>におけるイベント発生フローを調べてみました。

mouseover

mousedown

focus

mouseup

click

* 今回MacOSX10.7のSafari5.1.2 (7534.52.7)を使用しました。

先程の表にもあった通り、mouseover等はtouchデバイスでもディスパッチされており、ハンドラを定義すると動作します。
touchイベントも含めiOS検証端末での<input type=”text”>におけるイベント発生フローを調べてみました。

touchstart

(touchmove) * 0回以上

touchend

mouseover(初回時及び他の場所でタッチイベントがディスパッチされた後なら)

mousedown

focus

mouseup

click

先ほど調べたfocus()の正常動作したものと一致しました。
Mobile Safariではバーチャルキーボードを立ち上げる条件として、
これらのイベントハンドラからfocusメソッドを実行する必要がある様です。

touchstart, clickイベントからfocus()を呼び出す様にしてあげるというのは解ったのですが、
間にsetIntervalやsetTimeout等のwaitを入れる処理を挟むとfocus()は動かない様になります。
ただ、sleepの様な処理だと動作する様です。

NGな例

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    var setTimeoutHandler = function(){
        $('#testTextField1').focus();
    };
    var onClickHandler = function(){
        setTimeout(setTimeoutHandler, 1000);
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testButton').bind('click', onClickHandler);
});

OKな例

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    var onClickHandler = function(){
        var nowTime = (new Date()).getTime();
        var endTime = (new Date()).getTime() + 1000; //1000ms
        while(nowTime < endTime) {
            nowTime = (new Date()).getTime();
        }
        $('#testTextField1').focus();
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testButton').bind('click', onClickHandler);
});

スコープチェーンの問題かとふと思いましたので、以下を試してみてもNGでした。

NGな例

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    var setTimeoutHandler = function(){
        $('#testTextField1').focus();
    };
    var onClickHandler = function(){
        var _this = this;
        setTimeout(function(){
            setTimeoutHandler.apply(_this);
        }, 1000);
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testButton').bind('click', onClickHandler);
});

以下の様に元はclick等のイベントハンドラから別イベントを偽装する分には正常に動作しました。

OKな例

JavaScript (test.js)

$(document).ready(function(){
    var focusTextField = function(){
        console.log("focusElement");
    };
    var onKeypressHandler = function(){
        $('#testTextField1').focus();
    };
    var onClickHandler = function(){
        $('#testButton').trigger('keypress');
    };
    $('#testTextField1').bind('focus', focusTextField);
    $('#testButton').bind('click', onClickHandler);
    $('#testButton').bind('keypress', onKeypressHandler);
});

ブラウザの実装までは確認していないので、想像ですがブラウザでイベントの正当性を見ている様な気がしました。
なお、focus()の実行段階で対象(+親要素含む全て)の<textarea>や<input type=”text”>がdisplay:noneになっていると勿論動作しません。
ただ、表示画面内に無くても、HTML上でdisplay:noneで無ければ、focus対象まで自動的にスクロールされます。

以下、Mobile Safariでfocus()を正常に動かす為のポイントを掻い摘んでまとめますと、

  1. click/touchstartイベント(他のものも走るが実用的な観点からだとこの2つ)のハンドラからfocus()を実行する
  2. 上記ハンドラにsetTimeout等のwait処理を入れてはいけない。するのであれば、ループを用いたsleep処理をいれる
  3. focus段階で対象の<textarea>や<input type="text">がdisplay:noneになっていない様にする

となります。

大抵の場合は問題ないと思うのですが、『Mobile Safariでfocus()を実行しているが、キーボードが出現しない』という妙な現象に遭遇した際は、このエントリの事を思い出して頂けると幸いです。