フォーカス システム

フォーカス システムは、Blockly エディタでのユーザーの場所(フォーカス)を追跡します。Blockly とカスタムコードで、現在フォーカスがあるコンポーネント(ブロック、フィールド、ツールボックス カテゴリなど)を特定し、そのフォーカスを別のコンポーネントに移動するために使用されます。

カスタムコードがフォーカス システムで正しく動作するように、フォーカス システムを理解することが重要です。

アーキテクチャ

フォーカス システムは次の 3 つの部分で構成されています。

  • FocusManager は、Blockly 全体でフォーカスを調整するシングルトンです。Blockly とカスタムコードは、Blockly のフォーカスがあるコンポーネントを特定したり、Blockly のフォーカスを別のコンポーネントに移動したりするために、このメソッドを使用します。また、DOM フォーカス イベントをリッスンし、Blockly フォーカスと DOM フォーカスを同期し、どのコンポーネントにフォーカスがあるかを示す CSS クラスを管理します。

    フォーカス マネージャーは主に Blockly で使用されます。カスタムコードでフォーカス システムとやり取りするために使用されることがあります。

  • IFocusableTree は、ワークスペースやツールボックスなど、Blockly エディタの独立した領域です。ブロックやフィールドなどのフォーカス可能なノードで構成されています。ツリーにはサブツリーを含めることもできます。たとえば、メイン ワークスペースのブロックにあるミューテーター ワークスペースは、メイン ワークスペースのサブツリーです。

    IFocusableTree は主にフォーカス マネージャーで使用されます。カスタム ツールボックスを作成する場合を除き、実装する必要はないでしょう。

  • IFocusableNode は、ブロック、フィールド、ツールボックス カテゴリなど、フォーカスを設定できる Blockly コンポーネントです。フォーカス可能なノードには、ノードを表示する DOM 要素があり、ノードに Blockly フォーカスがある場合は DOM フォーカスがあります。ツリーはフォーカス可能なノードでもあります。たとえば、ワークスペース全体に焦点を当てることができます。

    IFocusableNode のメソッドは主にフォーカス マネージャーによって呼び出されます。

    IFocusableNode 自体は、フォーカスがあるコンポーネントを表すために使用されます。たとえば、ユーザーがブロックのコンテキスト メニューでアイテムを選択すると、ブロックは IFocusableNode としてアイテムのコールバック関数に渡されます。

    カスタム コンポーネントを作成する場合は、IFocusableNode を実装する必要がある場合があります。

フォーカスの種類

フォーカス システムは、さまざまな種類のフォーカスを定義します。

Blockly のフォーカスと DOM のフォーカス

フォーカスには、Blockly フォーカスと DOM フォーカスの 2 つの主なタイプがあります。

  • Blockly フォーカスは、どの Blockly コンポーネント(ブロック、フィールド、ツールボックス カテゴリなど)にフォーカスがあるかを指定します。Blockly コンポーネントのレベルで作業するために必要です。たとえば、キーボード ナビゲーション プラグインを使用すると、ユーザーは矢印キーを使用して、ブロックからフィールドなどのコンポーネント間を移動できます。同様に、コンテキスト メニュー システムは現在のコンポーネントに適したメニューを構築します。つまり、ワークスペース、ブロック、ワークスペース コメントに対して異なるメニューを構築します。

  • DOM フォーカスは、どの DOM 要素にフォーカスがあるかを指定します。DOM 要素レベルで作業するために必要です。たとえば、スクリーン リーダーは、現在 DOM フォーカスがある要素に関する情報を提示し、タブキーを押すと DOM 要素から DOM 要素にフォーカスが移動します。

フォーカス マネージャーは Blockly フォーカスと DOM フォーカスを同期させているため、ノード(Blockly コンポーネント)に Blockly フォーカスがある場合、その基盤となる DOM 要素には DOM フォーカスがあり、その逆も同様です。

アクティブ フォーカスとパッシブ フォーカス

Blockly のフォーカスは、アクティブ フォーカスパッシブ フォーカスにさらに分割されます。アクティブ フォーカスとは、ノードがキー押下などのユーザー入力を受け取ることを意味します。パッシブ フォーカスとは、ノードが以前はアクティブ フォーカスを持っていたが、ユーザーが別のツリーのノードに移動した(ワークスペースからツールボックスに移動したなど)か、Blockly エディタから完全に離れたときにフォーカスを失ったことを意味します。ツリーがフォーカスを取り戻すと、パッシブ フォーカスされたノードがアクティブ フォーカスを取り戻します。

各ツリーには個別のフォーカス コンテキストがあります。つまり、ツリー内のノードは 1 つまでしかフォーカスできません。フォーカスがアクティブかパッシブかは、ツリーにフォーカスがあるかどうかによって異なります。ページ全体でアクティブなフォーカスを持つノードは 1 つのみです。

フォーカス マネージャーは、アクティブにフォーカスされているノードとパッシブにフォーカスされているノードで異なるハイライト(CSS クラス)を使用します。これにより、ユーザーは現在地と戻る場所を把握できます。

エフェメラル フォーカス

一時的なフォーカスと呼ばれる別の種類のフォーカスがあります。ダイアログやフィールド エディタなどの個別のワークフローは、フォーカス マネージャーから一時的なフォーカスをリクエストします。フォーカス マネージャーが一時的なフォーカスを付与すると、フォーカス システムが一時停止します。実際には、このようなワークフローは、フォーカス システムが DOM フォーカス イベントを処理する可能性を心配することなく、DOM フォーカス イベントをキャプチャして処理できることを意味します。

フォーカス マネージャーが一時的なフォーカスを付与すると、アクティブにフォーカスされているノードがパッシブ フォーカスに変更されます。エフェメラル フォーカスが返されたときに、アクティブ フォーカスを復元します。

次の例は、Blockly がフォーカス システムを使用する方法を示しています。これらのガイドラインは、コードがフォーカス システムにどのように適合するか、また、コードがフォーカス システムをどのように使用するかを理解するのに役立ちます。

キーボードでフォーカスを移動する

2 つのフィールドを含むブロックに Blockly のフォーカスがある場合、ブロックの DOM 要素のハイライト(CSS クラス)で示されます。ユーザーが右矢印を押したとします。

  1. キーボード ナビゲーション プラグイン:
    • キー押下イベントを受け取ります。
    • ナビゲーション システム(コア Blockly の一部)に「次」のコンポーネントにフォーカスを移動するようリクエストします。
  2. ナビゲーション システム:
    • フォーカス マネージャーに、どのコンポーネントに Blockly フォーカスがあるかを問い合わせます。フォーカス マネージャーは、ブロックを IFocusableNode として返します。
    • IFocusableNodeBlockSvg であることを確認し、ブロックを移動するためのルールを確認します。このルールでは、Blockly のフォーカスをブロック全体からブロックの最初のフィールドに移動する必要があることが規定されています。
    • フォーカス マネージャーに、Blockly のフォーカスを最初のフィールドに移動するよう指示します。
  3. フォーカス マネージャー:
    • 状態を更新して、Blockly のフォーカスを最初のフィールドに設定します。
    • フィールドの DOM 要素に DOM フォーカスを設定します。
    • ハイライト クラスをブロックの要素からフィールドの要素に移動します。

マウスでフォーカスを移動する

ここで、ユーザーがブロックの 2 番目のフィールドをクリックしたとします。フォーカス マネージャー:

  1. 最初のフィールドの DOM 要素で DOM focusout イベントを受け取り、2 番目のフィールドの DOM 要素で focusin イベントを受け取ります。
  2. フォーカスを受け取った DOM 要素が 2 番目のフィールドに対応していることを確認します。
  3. 状態を更新して、Blockly のフォーカスを 2 番目のフィールドに設定します。(ブラウザがすでに DOM フォーカスを設定しているため、フォーカス マネージャーが DOM フォーカスを設定する必要はありません)。
  4. ハイライト クラスを最初のフィールドの要素から 2 番目のフィールドの要素に移動します。

その他の例

ほかにも以下のような例が考えられます。

  • ユーザーがツールボックスからワークスペースにブロックをドラッグすると、マウスイベント ハンドラが新しいブロックを作成し、フォーカス マネージャーを呼び出してそのブロックに Blockly フォーカスを設定します。

  • ブロックが削除されると、その dispose メソッドはフォーカス マネージャーを呼び出して、フォーカスをブロックの親に移動します。

  • キーボード ショートカットでは、IFocusableNode を使用して、ショートカットが適用される Blockly コンポーネントを識別します。

  • コンテキスト メニューは、IFocusableNode を使用して、メニューが呼び出された Blockly コンポーネントを特定します。

カスタマイズとフォーカス システム

Blockly をカスタマイズする場合は、コードがフォーカス システムで正しく動作することを確認する必要があります。フォーカス システムを使用して、現在フォーカスされているノードを特定して設定することもできます。

カスタムブロックとツールボックスの内容

Blockly をカスタマイズする最も一般的な方法は、カスタムブロックを定義して、ツールボックスの内容をカスタマイズすることです。これらのアクションは、フォーカス システムに影響しません。

カスタムクラス

カスタムクラスでは、フォーカス インターフェース(IFocusableTreeIFocusableNode)のいずれかまたは両方を実装する必要がある場合があります。この場合が常に明確であるとは限りません。

一部のクラスでは、フォーカス インターフェースを実装する必要があります。たとえば:

  • カスタム ツールボックスを実装するクラス。このクラスは IFocusableTreeIFocusableNode を実装する必要があります。

  • ユーザーが移動できる可視コンポーネント(フィールドやアイコンなど)を作成するクラス。これらのクラスは IFocusableNode を実装する必要があります。

一部のクラスは、表示可能なコンポーネントを作成しない場合や、ユーザーが移動できない表示可能なコンポーネントを作成する場合でも、IFocusableNode を実装する必要があります。たとえば:

  • IFocusableNode を拡張するインターフェースを実装するクラス。

    たとえば、キーボード ナビゲーション プラグインの移動アイコンは、矢印キーでブロックを移動できることを示す四方向の矢印を表示します。アイコン自体は表示されず(四方向矢印はバブル)、ユーザーはアイコンに移動できません。ただし、アイコンは IIcon を実装し、IIconIFocusableNode を拡張するため、アイコンは IFocusableNode を実装する必要があります。

  • IFocusableNode を必要とする API で使用されるクラス。

    たとえば、FlyoutSeparator クラスは、フライアウト内の 2 つのアイテム間にギャップを作成します。DOM 要素を作成しないため、表示されるコンポーネントがなく、ユーザーが移動することもできません。ただし、FlyoutItem に保存され、FlyoutItem コンストラクタで IFocusableNode が必要になるため、IFocusableNode を実装する必要があります。

  • IFocusableNode を実装するクラスを拡張するクラス。

    たとえば、ToolboxSeparatorToolboxItem を拡張し、ToolboxItemIFocusableNode を実装します。ツールボックスの区切り文字には表示されるコンポーネントがありますが、操作できず、有用なコンテンツもないため、ユーザーは区切り文字に移動できません。

他のクラスは、ユーザーが移動できる可視コンポーネントを作成しますが、IFocusableNode を実装する必要はありません。たとえば:

  • フィールド エディタやダイアログなど、独自のフォーカスを管理する可視コンポーネントを作成するクラス。(このようなクラスは、開始時に一時的なフォーカスを取得し、終了時にそれを返す必要があります。WidgetDiv または DropDownDiv を使用すると、この処理が自動的に行われます)。

最後に、一部のクラスはフォーカス システムとやり取りしないため、IFocusableTree または IFocusableNode を実装する必要はありません。たとえば:

  • ユーザーが移動したり操作したりできない可視コンポーネントを作成し、スクリーン リーダーが使用する可能性のある情報を含まないクラス。たとえば、ゲーム内の純粋な装飾的な背景などです。

  • IMetricsManager または IVariableMap を実装するクラスなど、フォーカス システムとまったく関係のないクラス。

クラスがフォーカス システムとやり取りするかどうか不明な場合は、キーボード ナビゲーション プラグインでテストします。これが失敗した場合は、IFocusableTree または IFocusableNode の実装が必要になることがあります。成功してもまだ確信が持てない場合は、クラスを使用するコードを読んで、どちらかのインターフェースが必要かどうか、または他のインタラクションがあるかどうかを確認します。

フォーカス インターフェースを実装する

IFocusableTree または IFocusableNode を実装する最も簡単な方法は、これらのインターフェースを実装するクラスを拡張することです。たとえば、カスタム ツールボックスを作成する場合は、IFocusableTreeIFocusableNode を実装する Toolbox を拡張します。カスタム フィールドを作成する場合は、IFocusableNode を実装する Field を拡張します。コードがベースクラスのフォーカス インターフェース コードと干渉しないことを確認してください。

フォーカス インターフェースを実装するクラスを拡張する場合、通常はメソッドをオーバーライドする必要はありません。最も一般的な例外は IFocusableNode.canBeFocused です。ユーザーがコンポーネントに移動しないようにする場合は、このメソッドをオーバーライドする必要があります。

フォーカス コールバック メソッド(IFocusableTreeonNodeFocusonTreeFocusonTreeBlurIFocusableNodeonNodeBlur)をオーバーライドする必要性はあまりありません。これらのメソッドからフォーカスを変更しようとすると(FocusManager.focusNode または FocusManager.focusTree を呼び出すと)、例外が発生することに注意してください。

カスタム コンポーネントをスクラッチで作成する場合は、フォーカス インターフェースを自分で実装する必要があります。詳細については、IFocusableTreeIFocusableNode のリファレンス ドキュメントをご覧ください。

クラスを実装したら、キーボード ナビゲーション プラグインに対してテストし、コンポーネントに移動できる(または移動できない)ことを確認します。

フォーカス マネージャーを使用する

一部のカスタムクラスでは、フォーカス マネージャーが使用されています。この操作を行う最も一般的な理由は、現在フォーカスされているノードを取得することと、別のノードにフォーカスすることです。フォーカス マネージャーを取得するには、Blockly.getFocusManager を呼び出します。

const focusManager = Blockly.getFocusManager();

現在フォーカスされているノードを取得するには、getFocusedNode を呼び出します。

const focusedNode = focusManager.getFocusedNode();
// Do something with the focused node.

別のノードにフォーカスを移動するには、focusNode を呼び出します。

// Move focus to a different block.
focusManager.focusNode(myOtherBlock);

ツリーにフォーカスを移動するには、focusTree を呼び出します。これにより、ツリーのルートノードにノード フォーカスも設定されます。

// Move focus to the main workspace.
focusManager.focusTree(myMainWorkspace);

フォーカス マネージャーを使用するもう 1 つの一般的な理由は、一時的なフォーカスを取得して返すことです。takeEphemeralFocus 関数は、エフェメラル フォーカスを返すために呼び出す必要があるラムダを返します。

const returnEphemeralFocus = focusManager.takeEphemeralFocus();
// Do something.
returnEphemeralFocus();

WidgetDiv または DropDownDiv を使用する場合、一時的なフォーカスは自動的に処理されます。

タブ位置

フォーカス システムは、すべてのツリー(メイン ワークスペース、ツールボックス、フライアウト ワークスペース)のルート要素にタブストップ(0tabindex)を設定します。これにより、ユーザーは Tab キーを使用して Blockly エディタのメイン領域を移動し、キーボード ナビゲーション プラグインを使用してこれらの領域内を移動できます。これらのタブストップは変更しないでください。変更すると、フォーカス マネージャーがタブストップを管理できなくなります。

一般に、Blockly で使用される他の DOM 要素にタブストップを設定することは避けるべきです。これは、タブキーを使用してエディタの領域間を移動し、矢印キーを使用してそれらの領域内を移動するという Blockly のモデルを妨げるためです。また、このようなタブストップは意図したとおりに動作しない可能性があります。これは、各フォーカス可能なノードが DOM 要素をフォーカス可能な要素として宣言するためです。フォーカス可能な要素の子孫にタブストップを設定し、ユーザーがその要素にタブ移動すると、フォーカス マネージャーは DOM フォーカスを宣言されたフォーカス可能な要素に移動します。

Blockly エディタ外のアプリケーションの要素にタブストップを設定しても安全です。ユーザーがエディタからそのような要素にタブ移動すると、フォーカス マネージャーは Blockly のフォーカスをアクティブからパッシブに変更します。アクセシビリティを確保するため、MDN の tabindex 属性の説明にある警告で推奨されているように、tabindex プロパティを 0 または -1 に設定する必要があります。

DOM フォーカス

ユーザー補助機能の観点から、アプリケーションは DOM 要素で focus メソッドを呼び出すことを避けるべきです。スクリーン リーダーのユーザーは、アプリケーション内の不明な場所に突然移動するため、混乱します。

さらに、フォーカス マネージャーは、フォーカス イベントに応答して、フォーカスされた要素の最も近い祖先または自身で、宣言されたフォーカス可能な要素に DOM フォーカスを設定します。これは、focus が呼び出された要素とは異なる場合があります。(Blockly エディタ外の要素で focus が呼び出された場合など、フォーカス可能な最も近い祖先または自身がない場合、フォーカス マネージャーはアクティブにフォーカスされているノードをパッシブ フォーカスに変更するだけです)。

Positionables

Positionable は、ワークスペースの上に配置され、IPositionable を実装するコンポーネントです。たとえば、backpack プラグインのゴミ箱やバックパックなどがあります。Positionable はまだフォーカス システムに統合されていません。