canvas要素のフォールバックコンテンツが支援技術に見えるまで(WebKit)

最近のWebブラウザ(IE9Firefox 13以降、いつからか忘れましたがGoogle Chromeなど)はcanvas要素のフォールバック・コンテンツをサポートしています。これらのブラウザはcanvas要素内に記述されたフォールバック・コンテンツに対してアクセシビリティ・オブジェクトを作るので、支援技術(スクリーン・リーダーなど)からフォールバック・コンテンツを利用することができます。

画面に表示されていない要素にアクセシビリティ・オブジェクトが作られるのが不思議だったので、WebKitWebCore)でアクセシビリティ・オブジェクトが作られる過程を調べてみました。

WebKitWebCore)は基本的に2つのことをやっています。1つはcanvas要素の子孫に対してフラグを立てることで、もう1つはフラグのたった要素は画面に表示されていなくても(描画オブジェクトが存在しなくても)アクセシビリティ・オブジェクトを作ることです。

では、canvas要素の子孫に対してフラグをたてるところから見ていきます。

1. canvas要素が描画ツリーに追加される時、isInCanvasSubtreeフラグを立てる

void HTMLCanvasElement::attach()
{
    setIsInCanvasSubtree(true);
    HTMLElement::attach();
}

attachメソッドはDOMノードを描画ツリー(render tree)に追加するときに呼ばれます。このメソッドはDOMノードに対するスタイルを取得して、適切な描画オブジェクト(RenderObject)を生成します。継承元のNodeオブジェクトには次のようなコメントが書かれています。

// Attaches this node to the rendering tree. This calculates the style to be applied to the node and creates an
// appropriate RenderObject which will be inserted into the tree (except when the style has display: none). This
// makes the node visible in the FrameView.

日本語にすると「(attachメソッドは)このノードを描画ツリーに追加する。これはノードに適用されるスタイルを計算し、適切なRenderObjectを生成する。RenderObjectは(display: noneでない限り)ツリーに追加されるだろう。これはノードをFrameViewの中に見えるようにする。」といった感じでしょうか。

脱線しましたがcanvas要素ではattachの実際の処理の前にフラグを立てています。

2. (canvas要素に限らず)ある要素が描画ツリーに追加される時、親要素のisInCanvasSubtreeフラグが立っていたら、自身のisInCanvasSubtreeフラグを立てる

void Element::attach()
{
    // 省略
    if (parentElement() && parentElement()->isInCanvasSubtree())
        setIsInCanvasSubtree(true);
    // 省略
    ContainerNode::attach();
    // 省略
}

これもattachメソッドの中でフラグを立てています。Element::attachはフラグを立てた後でスーパークラスであるContainerNodeのattachを呼び出しています。ContainerNode::attachは子ノードのattachメソッドを呼び出しているのでcanvas要素の子孫要素に対してisInCanvasSubtreeがtrueになることがわかります。

数学的帰納法風に言えば、「2.」はあるドミノが倒れたら子ドミノが倒れること、「1.」は実際にドミノが倒れることを意味しています。

フラグを立てる処理はこれでお終いです。

3. 描画オブジェクトのあるcanvas要素に対してアクセシビリティ・オブジェクトを作る

ここからはアクセシビリティ・オブジェクトが作られるときの話になります。

WebKitではアクセシビリティ・オブジェクトをAccessibilityObjectとそのサブクラスで表現します。今回は3種類のクラスが主に登場します。

  • 基底となるAccessibilityObject
  • DOMノードに対応したAccessibilityNodeObject
  • 描画オブジェクトに対応したAccessibilityRenderObject

これら3つは継承関係にあり、AccessibilityRenderObjectのスーパークラスはAccessibilityNodeObjectで、AccessiblityNodeObjectのスーパークラスはAccessibilityObjectです。

レンダリングエンジンの中でAccessibilityObjectも木構造を作ります(レンダリングエンジンにはいったいいくつ木構造が存在するんでしょうね)。AccessibilityObjectは子のアクセシビリティ・オブジェクトへのアクセスが発生すると、子供となるオブジェクトを探して、子供に対応したAccessibilityObjectを作ります。

また、DocumentごとにAccessibilityObjectの生成などを管理するAXObjectCacheというオブジェクトが作られます。

ドキュメント内の要素に対してアクセシビリティ・オブジェクトが作られるまでは次のようになります。

  • DocumentオブジェクトにAXObjectCacheオブジェクトが1つあります。
  • AXObjectCacheは、DocumentのFrameView(ドキュメントのレイアウトを管理するオブジェクト)からアクセシビリティ・オブジェクトのルートとなるオブジェクト(AccessibilityScrollView)を生成します。
    • ルートオブジェクトの生成はWebCoreではなくWebKit側から呼び出しています。
  • ルートオブジェクトの子供にDocument(の描画オブジェクト)のアクセシビリティ・オブジェクトとスクロールバーのアクセシビリティ・オブジェクト(AccessibilityScrollBar)が作られます。

描画オブジェクトに対するアクセシビリティ・オブジェクトはAccessibilityRenderObjectとなります。前述のとおり、AccessibilityObjectは子アクセシビリティ・オブジェクトへのアクセスが発生すると、子供を作っていきます。AccessibilityRenderObjectの場合は自身の描画オブジェクトの子描画オブジェクトに対して、アクセシビリティ・オブジェクトを作っていきます(addChildren())。この処理がcanvas要素の描画オブジェクトに達すればcanvas要素のアクセシビリティ・オブジェクトが作られます。

ということで3.にはcanvas要素特有の処理はありません。

4. canvas要素の子孫ノードは、描画オブジェクトがなくてもアクセシビリティ・オブジェクトを作る

AccessibilityRenderObjectは描画オブジェクトの子描画オブジェクトに対して、アクセシビリティ・オブジェクトを作っていきます(前述)。描画オブジェクトのない(画面に表示されていない)DOMノードのアクセシビリティ・オブジェクトは基本的に作られません。しかしcanvas要素であれば、子描画オブジェクトではなく子DOMノードからアクセシビリティ・オブジェクトを作ります。

具体的な流れを見ていきましょう。

AccessibilityRenderObject::addChildrenは毎度addCanvasChildrenというメソッドを呼び出しています。

void AccessibilityRenderObject::addChildren()
{
    // 省略
    
    for (RefPtr<AccessibilityObject> obj = firstChild(); obj; obj = obj->nextSibling())
        addChild(obj.get());

    // 省略
    addCanvasChildren()
    // 省略
}

addCanvasChildrenはもしアクセシビリティ・オブジェクトに対応するDOMノードがcanvas要素ならば、AccessibilityNodeObjectのaddChildrenを呼び出します(AccessibilityRenderObjectはAccessibilityNodeObjectのサブクラスでsした)。

void AccessibilityRenderObject::addCanvasChildren()
{
    if (!node() || !node()->hasTagName(canvasTag))
        return;

    // If it's a canvas, it won't have rendered children, but it might have accessible fallback content.
    // Clear m_haveChildren because AccessibilityNodeObject::addChildren will expect it to be false.
    ASSERT(!m_children.size());
    m_haveChildren = false;
    AccessibilityNodeObject::addChildren();
}

AccessibilityNodeObject::addChildrenは、アクセシビリティ・オブジェクトに対応するDOMノードがcanvas要素ならば、子DOMノードに対してアクセシビリティ・オブジェクトを生成します。

void AccessibilityNodeObject::addChildren()
{
    // 省略
    // The only time we add children from the DOM tree to a node with a renderer is when it's a canvas.
    if (renderer() && !m_node->hasTagName(canvasTag))
        return;

    for (Node* child = m_node->firstChild(); child; child = child->nextSibling())
        addChild(axObjectCache()->getOrCreate(child));
}

AXObjectCache::getOrCreateの中身は少し複雑です。

AccessibilityObject* AXObjectCache::getOrCreate(Node* node)
{
    if (!node)
        return 0;

    if (AccessibilityObject* obj = get(node))
        return obj;

    if (node->renderer())
        return getOrCreate(node->renderer());

    if (!node->parentElement())
        return 0;
    
    // It's only allowed to create an AccessibilityObject from a Node if it's in a canvas subtree.
    // Or if it's a hidden element, but we still want to expose it because of other ARIA attributes.
    bool inCanvasSubtree = node->parentElement()->isInCanvasSubtree();
    bool isHidden = !node->renderer() && isNodeAriaVisible(node);
    if (!inCanvasSubtree && !inObjectSubtree && !isHidden)
        return 0;

    RefPtr<AccessibilityObject> newObj = createFromNode(node);

    // 省略

    return newObj.get();
}

AXObjectCache::getOrCreateの処理を言葉にすると次のようになります。

  1. DOMノードがなければ作らない
  2. アクセシビリティ・オブジェクトを生成済みならばそれを返す
  3. 描画オブジェクトが存在すれば描画オブジェクトをもとにアクセシビリティ・オブジェクトを作って(getOrCreate(node->reanderer()))返す
  4. 親要素が存在しなければ作らない
  5. (親要素がcanvas要素の子孫でない)かつ(描画オブジェクトは存在しないがaria-hidden="false")ならば作らない
  6. DOMノードからアクセシビリティ・オブジェクトを作って(createFromNode)返す

「(親要素がcanvas要素の子孫でない)かつ(描画オブジェクトは存在しないがaria-hidden="false")ならば作らない」を「作る」に言い直すと「(親要素がcanvas要素の子孫)か(描画オブジェクトは存在しないがaria-hidden="false")ならば作る」になります。
createFromNodeはAccessibilityNodeObject::createを呼び出しているだけです。AccessibilityNodeObjectはDOMノードの種類を見てアクセシビリティ・オブジェクトのroleを決定しています(AccessibilityNodeObject::determineAccessibilityRole)。とはいうものの、描画オブジェクトから作られるAccessibilityRenderObjectのrole決定処理(AccessibilityRenderObject::determineAccessibilityRole)と比べると、限定的で、見劣りするのも確かです。

ともあれcanvas要素のフォールバック・コンテンツに対してアクセシビリティ・オブジェクトが作られるまでの流れがわかりました(たぶん…)。

蛇足:AXObjectCache::getOrCreateで親要素のフラグをチェックする理由

親要素のフラグをチェックする理由を考えると次の2つだと思います。

  • 1.ではcanvas要素自身にもフラグを立てた。canvas要素自身に描画オブジェクトがないのに(表示されていないのに)フォールバック・コンテンツのアクセシビリティ・オブジェクトを作るのはおかしい。DOMノードがcanvas要素ではなくcanvas要素の子孫ノードであることを確認する必要がある
  • 2.ではcanvas要素の子孫かどうかのフラグは要素レベルで立てたので、要素以外のDOMノードは自身のフラグではなく親要素のフラグを確認する必要がある