Ext JS 4 / キーボードナビゲーション

この稿はSenchaの公式ドキュメント Keybord Navigationの翻訳です。

キーボードナビゲーション

キーボードでのナビゲートは、時にはマウスカーソルを使うよりも高速ですし、パワーユーザーに対してもマウスが使いにくいユーザーにアクセシビリティを提供する点においても有用です。

比較的複雑なExt JSのレイアウトのサンプルをキーボードから完全にアクセス可能なアプリケーションに変換してみます。 また、マウスカーソルよりも速いキーボードによるナビゲーションができるようにキーボードショートカットを追加します。

このガイドの終わりまでには、キーボードナビゲーションがどこで最も必要とされるかと、 KeyNav, KeyMap, FocusManager でキーボードナビゲーションを活用する方法を理解できるでしょう。

はじめに

これから始めるファイルがこれです。 このファイルを解凍し、お好みのエディタでcomplex.htmlとcomplex.jsを開きます。 また、同じディレクトリにExt JS 4のコピーを解凍し、それを”ext”にリネームします。

フォーカスマネージャ

フォーカスマネージャは、基本的なキーボードナビゲーションを有効にするために非常に簡単な方法を提供します。 しかも、それは実装が簡単です。

Ext.FocusManager.enable(true);

Ext.onReady の中にこの行を書きます。 ここにtrueを渡すとフォーカスされたエリア(フォーカスフレームともいう)の周りに青いリングの形で”視覚的合図”を表示します。 このフォーカスフレームによりエリアにフォーカスがあるかを一目で確認することができます。 ユーザーを押すと、アプリケーションを入力してから、レベルとBackspaceキーをたどって行くか、レベルを上に移動して脱出するためにEnterを押して、”レベル”を上下に移動して入力します。 ユーザーがアプリケーションに入力するためにEnterキーを押すと”レベル”を上下に移動します、 Enterでは下のレベルに行き、BackspaceやEscキーでは上のレベルに行きます。 Tabキーは、兄弟要素(同じレベル上にあること)の間をジャンプするために使います。

FocusManagerだけでエレメントをナビゲートしてみてください。 できれば、マウスをオフにしてください。 そこそこ使えますが、ある領域にアクセスできない(gridなど)とか、スクリーンを動き回るのが非常に面倒なことがわかります。 “ショートカット”でこれに対処します。 “ショートカット”によって簡単にアプリケーションの特定のパネルにジャンプできるようになります。

どのパネルにショートカットを設定すべきかを決めるときは、次のような条件を考慮します。

  • 頻繁に使用されるか?
  • アンカーとして使用することができる – つまり、他の離れた場所を簡単に取得できるようにする足がかりを提供することができるか?
  • そこにナビゲートすることは直感的に感じられるか?

これらの少なくとも一つに該当するものがある場合、キーボードショートカットを提供しエンドユーザを支援します。

KeyNav

簡単なキーボードナビゲーションを提供するのがKeyNavの仕事です。 Ext JSアプリケーションをナビゲートするのに次のキーが使用できます。

  • Enter
  • Space
  • Left
  • Right
  • Up
  • Down
  • Tab
  • Escape
  • Page Up
  • Page Down
  • Delete
  • Backspace
  • Home
  • End

また、特定のキーがない制限されたキーボードがあることも心にとめておくべきです。

例えば、一部のAppleのコンピュータには、Page Up, Page Down, Del, Home, End キーがありません。 使い方の例を見てみましょう。

var nav = Ext.create('Ext.util.KeyNav', "my-element", {
    "left" : function(e){
        this.moveLeft(e.ctrlKey);
    },
    "right" : function(e){
        this.moveRight(e.ctrlKey);
    },
    "enter" : function(e){
        this.save();
    },
    scope : this
});

KeyNavの専門は、矢印キーのリッスンですので、 Tab, Enter, Esc キーを使う代わりに矢印キーでパネルをナビゲートする機能を追加してみます。

var nav = Ext.create('Ext.util.KeyNav', Ext.getBody(), {
    "left" : function(){
        var el = Ext.FocusManager.focusedCmp;
        if (el.previousSibling()) el.previousSibling().focus();
    },
    "right" : function(){
        var el = Ext.FocusManager.focusedCmp;
        if (el.nextSibling()) el.nextSibling().focus();
    },
    "up" : function() {
        var el = Ext.FocusManager.focusedCmp;
        if (el.up()) el.up().focus();
    },
    "down" : function() {
        var el = Ext.FocusManager.focusedCmp;
        if (el.items) el.items.items[0].focus();
    },
    scope : this
});

focusedCmp で現在フォーカスのあるコンポーネントを取得します。 関数がnullでない値を返したら、左矢印キーでは前の兄弟要素に、下矢印キーでは最初の子コンポーネントに、フォーカスを当てます。 これでアプリケーションのナビゲートがより簡単になりました。 次に、 Ext.util.KeyMap に注目し、キーに特定の機能を追加する方法を見ていきましょう。

KeyMap

サンプルのExtアプリケーションには多くの領域(Region: North, South, East, West)があることがわかります。 これらのエレメントにフォーカスを当てて、それが折りたたまれている場合にはそれを展開するKeyMapを作ってみましょう。 それでは、よくあるKeyMapオブジェクトがどのようなものか見てみましょう。

var map = Ext.create('Ext.util.KeyMap', "my-element", {
    key: 13, // or Ext.EventObject.ENTER
    ctrl: true,
    shift: false,
    fn: myHandler,
    scope: myObject
});

最初のプロパティkeyは、キーをマッピングする数値キーコードです。 どのキーにどの数字がマッピングされているかは こちら の文書に載っています。 次の二つ、ctrlとshiftは、それぞれのキーが押されているときに機能を有効にする場合にtrueを指定します。 この場合は、ctrlがtrueになってますので、ctrl+Enterが押されたときにmyHandlerが起動します。 fnが呼び出される関数です。インラインまたは関数への参照が指定できます。 最後のscopeはこのKeyMapが有効な場所を定義します。

KeyMapは汎用的で一つのキーで一つの機能を実行させることもキーの配列にに同じ機能を実行させることもできます。 いくつかのキーでmyHandlerを実行させたい場合は、key: [10, 13] のように記述します。

まずはメインパネル(north, south, east, west)から始めます。

var map = Ext.create('Ext.util.KeyMap', Ext.getBody(), [
    {
        key: Ext.EventObject.E, // E for east
        shift: true,
        ctrl: false, // 衝突を避けるため明示的にfalseを設定する
        fn: function() {
            var parentPanel = eastPanel;
            expand(parentPanel);
        }
    },
    {
        key: Ext.EventObject.W, // W for west
        shift: true,
        ctrl: false,
        fn: function() {
            var parentPanel = westPanel;
            expand(parentPanel);
        }
    },
    {
        key: Ext.EventObject.S, // S for south
        shift: true,
        ctrl: false,
        fn: function() {
            var parentPanel = southPanel;
            expand(parentPanel);
        }
    }
]);

どのキーをリッスンしているかわかりやすくするためにExt.EventObject.Xを使っています。 残りも上記の例のように記述してください。 次に expand() 関数を次のように書きます。

function expand(parentPanel) {
    parentPanel.toggleCollapse();
    parentPanel.on('expand', function(){
        parentPanel.el.focus();
    });
    parentPanel.on('collapse', function(){
        viewport.el.focus();
    });
}

この関数は、パネルの折りたたみを切り替え、展開される時にはそこにフォーカスを当て、 既に展開されている場合は折りたたんで、次の上位レベル(Viewport)にフォーカスを返します。

ここですべてのコードができたので、キーを押してパネルの折りたたみを切り替えて、 展開/折りたたみの小さなボタンをクリックして切り替えるのと比べてみてください。 キーボードでやる方がはるかに速いですね。

次に、westのパネル上のNavigation, Settings, Informatin タブで同じような過程で作業します。 それらは既出のparentPanelの子要素ですからsubPanelと呼びます。

{
    key: Ext.EventObject.S, // S for settings
    ctrl: true,
    fn: function() {
        var parentPanel = westPanel;
        var subPanel = settings;
        expand(parentPanel, subPanel);
    }
},
{
    key: Ext.EventObject.I, // I for information
    ctrl: true,
    fn: function() {
        var parentPanel = westPanel;
        var subPanel = information;
        expand(parentPanel, subPanel);
    }
},
{
    key: Ext.EventObject.N, // N for navigation
    ctrl: true,
    fn: function(){
        var parentPanel = westPanel;
        var subPanel = navigation;
        expand(parentPanel, subPanel);
    }
}

これまで利用してきたのと同じパターンに従いますが、subPanelという変数を追加します。 expand()関数はこれは何のためのものかわからないので、関数をリファクタリングしてsubPanelが宣言されているかどうかに応じて処理を実行するようにします。

function expand(parentPanel, subPanel) {

    if (subPanel) {
        function subPanelExpand(subPanel) {
            // set listener for expand function
            subPanel.on('expand', function() {
                setTimeout(function() { subPanel.focus(); }, 200);
            });
            // expand the subPanel
            subPanel.expand();
        }

        if (parentPanel.collapsed) {
            // enclosing panel is collapsed, open it
            parentPanel.expand();
            subPanelExpand(subPanel);
        }
        else if (!subPanel.collapsed) {
            // subPanel is open and just needs focusing
            subPanel.focus();
        }
        else {
            // parentPanel isn't collapsed but subPanel is
            subPanelExpand(subPanel);
        }
    }
    else {
        // no subPanel detected
        parentPanel.toggleCollapse();
        parentPanel.on('expand', function(){
            parentPanel.el.focus();
        });
        parentPanel.on('collapse', function(){
            viewport.el.focus();
        });
    }
}

focus()はパネルが展開された後に発火するexpandイベントリスナーの中にあるにもかかわらず、 setTimeoutでラッピングされる必要があります。 さもなければ、フォーカスが当たるのが速すぎる (展開される間にフォーカスがあたる) ため、フォーカスフレームがパネルよりも小さくなってしまうからです。 この現象を回避するために200ミリ秒遅延しています。 この問題はparentPanelでは発生しません。

この時点では、キーボードだけで(例:Shift + E や Ctrl + S)パネルを開閉するだけでなく、フォーカスを当てることができます。 当然のことながら、キーを押すことでExt JSの任意の関数をトリガすることができ、 自然な感じのアプリケーションを作成することができます。

KeyMapに最後のオブジェクトを追加します。二つのタブがあります。一つはセンターパネル上にあり、 もう一つはEast領域にある’Eye Data’タブです。 これらをCtrl + wでブラウザのタブと同じように閉じることができると便利です。それにはは次のようにします。

{
    key: Ext.EventObject.W, // W to close
    ctrl: true,
    fn: function(){
        var el = Ext.FocusManager.focusedCmp;
        if (el.xtype === 'tab' && el.closable) {
            el.up().focus();
            el.destroy();
        }
    },
    scope: this
}

どのキーをリッスンするか設定し、フォーカスマネージャのfocusedCmpプロパティで現在フォーカスが当たっているコンポーネント取得します。 現在フォーカスのあるコンポーネントはタブで、それがクローズできる場合には、親パネルにフォーカスを設定し、タブを破棄します。

グリッドを修正する

グリッド内の行にフォーカスを当てようとすると、マウスなしでできないことにお気づきでしょう。 コンソールを見ると理由の手掛かりを得ることができます、それは”pos is undefined” であるとレポートしています。 クリックイベントは、グリッド内の位置がどこであるかを含め、レコードに関する情報を渡します。 FocusManagerを使うと、この情報は渡されませんので、rowとcolumnプロパティを指定するオブジェクトを渡すことでエミュレートする必要があります。 viewport変数の下部に次のものを実行します。

var easttab = Ext.getCmp('easttab');

var gridMap = Ext.create('Ext.util.KeyMap', 'eastPanel', [
    {
        key: '\r', // Return key
        fn: function() {
            easttab.getSelectionModel().setCurrentPosition({row: 0, column: 1});
        },
        scope: 'eastPanel'
    },
    {
        key: Ext.EventObject.ESC,
        fn: function() {
            easttab.el.focus();
        },
        scope: 'eastPanel'
    }
]);

これを試すと、正常にキーボードを使用してグリッドに出入りできることがわかります。 これらのキーのscopeを指定することが重要です。さもなくばグリッドから脱出することはできません。

キーボードマッピングを切り替える

KeyMapの便利な機能は簡単に有効化または無効化できることです。

アプリケーションのユーザーは、ほとんどの場合フォーカスフレームが必要ないと思われますので、 この動作を有効にするボタン(または別のキーマップ)を作ることができます。

キーボードナビゲーションをオン/オフするグローバルなキープレスを追加したい場合は、次のようにします。

var initMap = Ext.create('Ext.util.KeyMap', Ext.getBody(), {
    key: Ext.EventObject.T, // T for toggle
    shift: true,
    fn: function(){
        map.enabled ? map.disable() : map.enable();
        Ext.FocusManager.enabled ? Ext.FocusManager.disable() : Ext.FocusManager.enable(true);
    }
});

シフト+ Tでキーボードナビゲーションをオフにし、同じ操作でオンに戻す新しいKeyMapを作成しました。 ここで既存のKeyMapを使用することはできません、なぜならKeyMap自体がオフにされるので、再度有効にすることができないからです。

結論

キーボードを使ってアクセスできなかった複雑な一連のパネルをアクセス可能に変更しました。 また、フォーカスマネージャ上にカスタム関数を追加する必要があった例が発生しました。

KeyMapによって、別のパネルにジャンプするだけでなく、通常のキー入力で動作させたいすべての機能を実行ことができることを学びました。 最後に、KeyNavによって、矢印キーを使用してアプリケーション内を移動することがいかに簡単であるかを見てきました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です