Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
MediaQueryList.matches should update parent document layout for viewp…
…ort-dependent media queries

https://bugs.webkit.org/show_bug.cgi?id=189583

Reviewed by Antti Koivisto.

This patch makes MediaQueryList::matches update the associated document's owner element's layout
if the associated media query set contains a viewport dependent expression.

New behavior matches the spec and the behavior of Blink although Blink fails the last test case
in the newly introduced W3C style testharness.js test.

* LayoutTests/fast/css/media-query-matches-in-iframe-expected.txt: Added.
* LayoutTests/fast/css/media-query-matches-in-iframe.html: Added.
* LayoutTests/printing/print-with-media-query-destory-expected.txt: Rebaselined.

* Source/WebCore/css/MediaQueryEvaluator.cpp:
(WebCore::isViewportDependent): Moved to MediaQueryExpression.cpp.
(WebCore::MediaQueryEvaluator::evaluate const):

* Source/WebCore/css/MediaQueryExpression.cpp:
(WebCore::MediaQueryExpression::isViewportDependent const): Moved from MediaQueryEvaluator.cpp.
* Source/WebCore/css/MediaQueryExpression.h:

* Source/WebCore/css/MediaQueryList.cpp:
(WebCore::MediaQueryList::evaluate): This function now dispatches an event on its own instead of
having an out argument for MediaQueryMatcher to use. It now takes MediaQueryMatcher::EventMode
which specifies whether we should be synchronously dispatching a change event or not.
(WebCore::MediaQueryList::matches): Update the layout of the owner element's document
if the associated media query set contains a viewport dependent expression.
* Source/WebCore/css/MediaQueryList.h:
(WebCore::MediaQueryList::m_needsNotification): Added.

* Source/WebCore/css/MediaQueryMatcher.cpp:
(WebCore::MediaQueryMatcher::evaluateAll): Now takes EventMode which specifies whether we're
updating the rendering in which case the event should be dispatched now, or we're updating
for viewport dependent expression in one of the media queries.
* Source/WebCore/css/MediaQueryMatcher.h:

* Source/WebCore/dom/Document.cpp:
(WebCore::Document::evaluateMediaQueriesAndReportChanges):

Canonical link: https://commits.webkit.org/253123@main
  • Loading branch information
rniwa committed Aug 4, 2022
1 parent 6b27b1f commit e66751c
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 39 deletions.
24 changes: 24 additions & 0 deletions LayoutTests/fast/css/media-query-matches-in-iframe-expected.txt
@@ -0,0 +1,24 @@

PASS matchMedia('(max-width: 150px)').matches should update immediately
PASS matchMedia('(width: 100px)').matches should update immediately
PASS matchMedia('(orientation: portrait)').matches should update immediately
PASS matchMedia('(aspect-ratio: 1/1)').matches should update immediately
PASS matchMedia('(max-aspect-ratio: 4/3)').matches should update immediately
PASS matchMedia('(height: 100px)').matches should update immediately
PASS matchMedia('(max-height: 150px)').matches should update immediately
PASS matchMedia('(min-aspect-ratio: 3/4)').matches should update immediately
PASS matchMedia('(min-height: 150px)').matches should update immediately
PASS matchMedia('(aspect-ratio: 1/2)').matches should update immediately
PASS matchMedia('(min-width: 150px)').matches should update immediately
PASS matchMedia('(min-aspect-ratio: 4/3)').matches should update immediately
PASS matchMedia('(max-width: 150px)') should not receive a change event until update the rendering step of HTML5 event loop
PASS matchMedia('(width: 100px)') should not receive a change event until update the rendering step of HTML5 event loop
PASS matchMedia('(orientation: portrait)') should not receive a change event until update the rendering step of HTML5 event loop
PASS matchMedia('(aspect-ratio: 1/1)') should not receive a change event until update the rendering step of HTML5 event loop
PASS matchMedia('(max-aspect-ratio: 4/3)') should not receive a change event until update the rendering step of HTML5 event loop
PASS matchMedia('(max-width: 150px)') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called
PASS matchMedia('(width: 100px)') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called
PASS matchMedia('(orientation: portrait)') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called
PASS matchMedia('(aspect-ratio: 1/1)') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called
PASS matchMedia('(max-aspect-ratio: 4/3)') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called

103 changes: 103 additions & 0 deletions LayoutTests/fast/css/media-query-matches-in-iframe.html
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<body>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>

async function createFrameAndUpdateLayout(test) {
const iframe = await new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.style.width = '100px';
iframe.style.height = '100px';
iframe.onload = () => resolve(iframe);
document.body.appendChild(iframe);
test.add_cleanup(() => iframe.remove());
});
iframe.contentDocument.body.innerHTML = '<span>some content</span>';
window.preventOptimization1 = iframe.getBoundingClientRect();
window.preventOptimization2 = iframe.contentDocument.querySelector('span').getBoundingClientRect();
return iframe;
}

for (const query of ['(max-width: 150px)', '(width: 100px)', '(orientation: portrait)', '(aspect-ratio: 1/1)', '(max-aspect-ratio: 4/3)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
assert_true(mediaQuery.matches);
iframe.style.width = '200px';
assert_false(mediaQuery.matches);
}, `matchMedia('${query}').matches should update immediately`);
}

for (const query of ['(height: 100px)', '(max-height: 150px)', '(min-aspect-ratio: 3/4)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
assert_true(mediaQuery.matches);
iframe.style.height = '200px';
assert_false(mediaQuery.matches);
}, `matchMedia('${query}').matches should update immediately`);
}

for (const query of ['(min-height: 150px)', '(aspect-ratio: 1/2)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
assert_false(mediaQuery.matches);
iframe.style.height = '200px';
assert_true(mediaQuery.matches);
}, `matchMedia('${query}').matches should update immediately`);
}

for (const query of ['(min-width: 150px)', '(min-aspect-ratio: 4/3)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
assert_false(mediaQuery.matches);
iframe.style.width = '200px';
assert_true(mediaQuery.matches);
}, `matchMedia('${query}').matches should update immediately`);
}

for (const query of ['(max-width: 150px)', '(width: 100px)', '(orientation: portrait)', '(aspect-ratio: 1/1)', '(max-aspect-ratio: 4/3)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
let changes = 0;
mediaQuery.addEventListener('change', () => ++changes);
assert_true(mediaQuery.matches);
assert_equals(changes, 0);
iframe.style.width = '200px';
assert_false(mediaQuery.matches);
assert_equals(changes, 0);
await new Promise(requestAnimationFrame);
await new Promise(setTimeout);
assert_false(mediaQuery.matches);
assert_equals(changes, 1);
}, `matchMedia('${query}') should not receive a change event until update the rendering step of HTML5 event loop`);
}

for (const query of ['(max-width: 150px)', '(width: 100px)', '(orientation: portrait)', '(aspect-ratio: 1/1)', '(max-aspect-ratio: 4/3)']) {
promise_test(async function () {
const iframe = await createFrameAndUpdateLayout(this);
const mediaQuery = iframe.contentWindow.matchMedia(query);
const events = [];
iframe.contentWindow.addEventListener('resize', () => {
assert_array_equals(events, []);
events.push('resize');
});
mediaQuery.addEventListener('change', () => events.push('change'));
assert_true(mediaQuery.matches);
assert_array_equals(events, []);
iframe.style.width = '200px';
assert_false(mediaQuery.matches);
assert_array_equals(events, []);
await new Promise(requestAnimationFrame);
assert_false(mediaQuery.matches);
assert_array_equals(events, ['resize', 'change']);
}, `matchMedia('${query}') should receive a change event after resize event on the window but before a requestAnimationFrame callback is called`);
}

</script>
</body>
@@ -1,2 +1 @@
Pass if no crash or assert

16 changes: 1 addition & 15 deletions Source/WebCore/css/MediaQueryEvaluator.cpp
Expand Up @@ -89,20 +89,6 @@ static bool isAccessibilitySettingsDependent(const AtomString& mediaFeature)
|| mediaFeature == MediaFeatureNames::prefersContrast;
}

static bool isViewportDependent(const AtomString& mediaFeature)
{
return mediaFeature == MediaFeatureNames::width
|| mediaFeature == MediaFeatureNames::height
|| mediaFeature == MediaFeatureNames::minWidth
|| mediaFeature == MediaFeatureNames::minHeight
|| mediaFeature == MediaFeatureNames::maxWidth
|| mediaFeature == MediaFeatureNames::maxHeight
|| mediaFeature == MediaFeatureNames::orientation
|| mediaFeature == MediaFeatureNames::aspectRatio
|| mediaFeature == MediaFeatureNames::minAspectRatio
|| mediaFeature == MediaFeatureNames::maxAspectRatio;
}

static bool isAppearanceDependent(const AtomString& mediaFeature)
{
return mediaFeature == MediaFeatureNames::prefersDarkInterface
Expand Down Expand Up @@ -183,7 +169,7 @@ bool MediaQueryEvaluator::evaluate(const MediaQuerySet& querySet, MediaQueryDyna
for (; j < expressions.size(); ++j) {
bool expressionResult = evaluate(expressions[j]);
if (dynamicResults) {
if (isViewportDependent(expressions[j].mediaFeature())) {
if (expressions[j].isViewportDependent()) {
isDynamic = true;
dynamicResults->viewport.append({ expressions[j], expressionResult });
}
Expand Down
14 changes: 14 additions & 0 deletions Source/WebCore/css/MediaQueryExpression.cpp
Expand Up @@ -275,6 +275,20 @@ MediaQueryExpression::MediaQueryExpression(const String& feature, CSSParserToken
}
}

bool MediaQueryExpression::isViewportDependent() const
{
return m_mediaFeature == MediaFeatureNames::width
|| m_mediaFeature == MediaFeatureNames::height
|| m_mediaFeature == MediaFeatureNames::minWidth
|| m_mediaFeature == MediaFeatureNames::minHeight
|| m_mediaFeature == MediaFeatureNames::maxWidth
|| m_mediaFeature == MediaFeatureNames::maxHeight
|| m_mediaFeature == MediaFeatureNames::orientation
|| m_mediaFeature == MediaFeatureNames::aspectRatio
|| m_mediaFeature == MediaFeatureNames::minAspectRatio
|| m_mediaFeature == MediaFeatureNames::maxAspectRatio;
}

String MediaQueryExpression::serialize() const
{
if (m_serializationCache.isNull())
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/css/MediaQueryExpression.h
Expand Up @@ -48,6 +48,7 @@ class MediaQueryExpression {
CSSValue* value() const;

bool isValid() const;
bool isViewportDependent() const;

String serialize() const;

Expand Down
46 changes: 38 additions & 8 deletions Source/WebCore/css/MediaQueryList.cpp
Expand Up @@ -22,6 +22,7 @@

#include "AddEventListenerOptions.h"
#include "EventNames.h"
#include "MediaQueryListEvent.h"
#include <wtf/IsoMallocInlines.h>

namespace WebCore {
Expand Down Expand Up @@ -78,16 +79,23 @@ void MediaQueryList::removeListener(RefPtr<EventListener>&& listener)
removeEventListener(eventNames().changeEvent, *listener, { });
}

void MediaQueryList::evaluate(MediaQueryEvaluator& evaluator, bool& notificationNeeded)
void MediaQueryList::evaluate(MediaQueryEvaluator& evaluator, MediaQueryMatcher::EventMode eventMode)
{
if (!m_matcher) {
notificationNeeded = false;
return;
}

RELEASE_ASSERT(m_matcher);
if (m_evaluationRound != m_matcher->evaluationRound())
setMatches(evaluator.evaluate(m_media.get()));
notificationNeeded = m_changeRound == m_matcher->evaluationRound();

m_needsNotification = m_changeRound == m_matcher->evaluationRound() || m_needsNotification;
if (!m_needsNotification || eventMode == MediaQueryMatcher::EventMode::Schedule)
return;
ASSERT(eventMode == MediaQueryMatcher::EventMode::DispatchNow);

RefPtr document = dynamicDowncast<Document>(scriptExecutionContext());
if (document && document->quirks().shouldSilenceMediaQueryListChangeEvents())
return;

dispatchEvent(MediaQueryListEvent::create(eventNames().changeEvent, media(), matches()));
m_needsNotification = false;
}

void MediaQueryList::setMatches(bool newValue)
Expand All @@ -104,7 +112,29 @@ void MediaQueryList::setMatches(bool newValue)

bool MediaQueryList::matches()
{
if (m_matcher && m_evaluationRound != m_matcher->evaluationRound())
if (!m_matcher)
return m_matches;

if (RefPtr document = dynamicDowncast<Document>(scriptExecutionContext())) {
if (RefPtr ownerElement = document->ownerElement()) {
bool isViewportDependent = [&]() {
for (auto& query : m_media->queryVector()) {
for (auto& expression : query.expressions()) {
if (expression.isViewportDependent())
return true;
}
}
return false;
}();

if (isViewportDependent) {
ownerElement->document().updateLayout();
m_matcher->evaluateAll(MediaQueryMatcher::EventMode::Schedule);
}
}
}

if (m_evaluationRound != m_matcher->evaluationRound())
setMatches(m_matcher->evaluate(m_media.get()));
return m_matches;
}
Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/css/MediaQueryList.h
Expand Up @@ -44,7 +44,7 @@ class MediaQueryList final : public RefCounted<MediaQueryList>, public EventTarg
void addListener(RefPtr<EventListener>&&);
void removeListener(RefPtr<EventListener>&&);

void evaluate(MediaQueryEvaluator&, bool& notificationNeeded);
void evaluate(MediaQueryEvaluator&, MediaQueryMatcher::EventMode);

void detachFromMatcher();

Expand Down Expand Up @@ -72,6 +72,7 @@ class MediaQueryList final : public RefCounted<MediaQueryList>, public EventTarg
unsigned m_changeRound; // Used to know if the query has changed in the last style selector change.
bool m_matches;
bool m_hasChangeEventListener { false };
bool m_needsNotification { false };
};

}
15 changes: 3 additions & 12 deletions Source/WebCore/css/MediaQueryMatcher.cpp
Expand Up @@ -28,7 +28,6 @@
#include "MediaList.h"
#include "MediaQueryEvaluator.h"
#include "MediaQueryList.h"
#include "MediaQueryListEvent.h"
#include "MediaQueryParserContext.h"
#include "NodeRenderStyle.h"
#include "RenderElement.h"
Expand Down Expand Up @@ -104,7 +103,7 @@ RefPtr<MediaQueryList> MediaQueryMatcher::matchMedia(const String& query)
return MediaQueryList::create(*m_document, *this, WTFMove(media), matches);
}

void MediaQueryMatcher::evaluateAll()
void MediaQueryMatcher::evaluateAll(EventMode eventMode)
{
ASSERT(m_document);

Expand All @@ -120,16 +119,8 @@ void MediaQueryMatcher::evaluateAll()

auto mediaQueryLists = m_mediaQueryLists;
for (auto& list : mediaQueryLists) {
if (!list)
continue;
bool notify;
list->evaluate(evaluator, notify);
if (notify) {
if (m_document && m_document->quirks().shouldSilenceMediaQueryListChangeEvents())
continue;

list->dispatchEvent(MediaQueryListEvent::create(eventNames().changeEvent, list->media(), list->matches()));
}
if (RefPtr protectedList = list.get())
protectedList->evaluate(evaluator, eventMode);
}
}

Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/css/MediaQueryMatcher.h
Expand Up @@ -50,7 +50,8 @@ class MediaQueryMatcher final : public RefCounted<MediaQueryMatcher> {

unsigned evaluationRound() const { return m_evaluationRound; }

void evaluateAll();
enum class EventMode : uint8_t { Schedule, DispatchNow };
void evaluateAll(EventMode);

bool evaluate(const MediaQuerySet&);

Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/dom/Document.cpp
Expand Up @@ -4313,7 +4313,7 @@ void Document::evaluateMediaQueriesAndReportChanges()
if (!m_mediaQueryMatcher)
return;

m_mediaQueryMatcher->evaluateAll();
m_mediaQueryMatcher->evaluateAll(MediaQueryMatcher::EventMode::DispatchNow);
}

void Document::updateViewportUnitsOnResize()
Expand Down

0 comments on commit e66751c

Please sign in to comment.