Skip to content

"in-view center point" is broken when an inline element contains block-level elements #1961

@julienw

Description

@julienw

In chapter 12. Elements, section 12.1 Interactability:
The first step of the algorithm to calculate in-view center point says:

1. Let rectangle be the first object of the DOMRect collection returned by calling getClientRects() on element. 

That rectangle is then used to find the center of the element.

The problem is when that first object has zero width and height, when another of the returned rects in the collection contain the main part of the element. This happens when an inline element such as a <a> contains block-level elements.

When coordinates are not integer, the final floor operation will yield coordinates that are outside the element. The floor operation doesn't make a lot of sense to me (the user can click on non-integer CSS pixels when the dpi isn't 1), but just removing this floor operation wouldn't be good enough: clicks would work, but they wouldn't be at the center of the element.

So I think the step 1. above should be changed to:

1. Let rectangle be the first object of the DOMRect collection returned by calling getClientRects() on element, that has non-null width and height. If all have a null width and height, let rectangle be the first object of the returned collection.

(maybe this could be extracted to another algorithm)

Here is a test that demonstrates the problem in Firefox:

def test_inline_element_with_zero_size_rects_at_fractional_position(
    session, inline
):
    # An inline <a> wrapping a block-level child produces zero-size line-box
    # rects at the start and end of its getClientRects() list: [0x0, 32x32,
    # 0x0]. Placing the element at a fractional y-coordinate (via
    # margin-top: 0.5px) means Math.floor on the zero-size rect's y lands
    # outside the element and elementsFromPoint misses it.
    # This test verifies that clicking such an element succeeds despite the
    # zero-size first rect, i.e. that the non-zero rect is used to determine
    # the element's position.
    session.url = inline("""
        <style>
          body { margin: 0; }
        </style>
        <div style="margin-top: 0.5px">
          <a id="link" href="#"
             onclick="document.getElementById('log').textContent = 'clicked'">
            <div style="width: 32px; height: 32px;"></div>
          </a>
        </div>
        <div id="log"></div>
    """)
    element = session.find.css("#link", all=False)

    # Verify the layout actually produces zero-size first rect; if this fails
    # the test premise has changed and the test should be revisited.
    first_rect = session.execute_script(
        """
        const r = arguments[0].getClientRects()[0];
        return { w: r.width, h: r.height };
        """,
        args=[element],
    )
    assert first_rect["w"] == 0 and first_rect["h"] == 0, (
        "expected zero-size first rect, got {}x{}".format(
            first_rect["w"], first_rect["h"]
        )
    )

    response = element_click(session, element)
    assert_success(response)

    log = session.find.css("#log", all=False)
    assert log.property("textContent") == "clicked"

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions