diff --git a/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt b/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt
new file mode 100644
index 0000000000..9a3f941bad
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt
@@ -0,0 +1,2 @@
+ contentSize: 100px x 200px; borderBoxSize [inline=140px, block=240px]; contentBoxSize [inline=100px, block=200px]; deviceBoxSize [inline=140px, block=240px]
+contentSize: 100px x 200px; borderBoxSize [inline=140px, block=280px]; contentBoxSize [inline=100px, block=200px]; deviceBoxSize [inline=140px, block=280px]
diff --git a/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt b/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt
new file mode 100644
index 0000000000..c9398473cd
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt
@@ -0,0 +1,2 @@
+ Size changed: 200px x 200px
+Size changed: 400px x 400px
diff --git a/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html b/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html
new file mode 100644
index 0000000000..6d5d8716e1
--- /dev/null
+++ b/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/input/ResizeObserver/observe.html b/Tests/LibWeb/Text/input/ResizeObserver/observe.html
new file mode 100644
index 0000000000..aab45650c4
--- /dev/null
+++ b/Tests/LibWeb/Text/input/ResizeObserver/observe.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp
index 9eb02dbddf..940b677875 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Document.cpp
@@ -4138,4 +4138,134 @@ String Document::query_command_value(String)
return String {};
}
+// https://drafts.csswg.org/resize-observer-1/#calculate-depth-for-node
+static size_t calculate_depth_for_node(Node const& node)
+{
+ // 1. Let p be the parent-traversal path from node to a root Element of this element’s flattened DOM tree.
+ // 2. Return number of nodes in p.
+
+ size_t depth = 0;
+ for (auto const* current = &node; current; current = current->parent())
+ ++depth;
+ return depth;
+}
+
+// https://drafts.csswg.org/resize-observer-1/#gather-active-observations-h
+void Document::gather_active_observations_at_depth(size_t depth)
+{
+ // 1. Let depth be the depth passed in.
+
+ // 2. For each observer in [[resizeObservers]] run these steps:
+ for (auto const& observer : m_resize_observers) {
+ // 1. Clear observer’s [[activeTargets]], and [[skippedTargets]].
+ observer->active_targets().clear();
+ observer->skipped_targets().clear();
+
+ // 2. For each observation in observer.[[observationTargets]] run this step:
+ for (auto const& observation : observer->observation_targets()) {
+ // 1. If observation.isActive() is true
+ if (observation->is_active()) {
+ // 1. Let targetDepth be result of calculate depth for node for observation.target.
+ auto target_depth = calculate_depth_for_node(*observation->target());
+
+ // 2. If targetDepth is greater than depth then add observation to [[activeTargets]].
+ if (target_depth > depth) {
+ observer->active_targets().append(observation);
+ } else {
+ // 3. Else add observation to [[skippedTargets]].
+ observer->skipped_targets().append(observation);
+ }
+ }
+ }
+ }
+}
+
+// https://drafts.csswg.org/resize-observer-1/#broadcast-active-resize-observations
+size_t Document::broadcast_active_resize_observations()
+{
+ // 1. Let shallowestTargetDepth be ∞
+ auto shallowest_target_depth = NumericLimits::max();
+
+ // 2. For each observer in document.[[resizeObservers]] run these steps:
+ for (auto const& observer : m_resize_observers) {
+ // 1. If observer.[[activeTargets]] slot is empty, continue.
+ if (observer->active_targets().is_empty()) {
+ continue;
+ }
+
+ // 2. Let entries be an empty list of ResizeObserverEntryies.
+ Vector> entries;
+
+ // 3. For each observation in [[activeTargets]] perform these steps:
+ for (auto const& observation : observer->active_targets()) {
+ // 1. Let entry be the result of running create and populate a ResizeObserverEntry given observation.target.
+ auto entry = ResizeObserver::ResizeObserverEntry::create_and_populate(realm(), *observation->target()).release_value_but_fixme_should_propagate_errors();
+
+ // 2. Add entry to entries.
+ entries.append(entry);
+
+ // 3. Set observation.lastReportedSizes to matching entry sizes.
+ switch (observation->observed_box()) {
+ case Bindings::ResizeObserverBoxOptions::BorderBox:
+ // Matching sizes are entry.borderBoxSize if observation.observedBox is "border-box"
+ observation->last_reported_sizes() = entry->border_box_size();
+ break;
+ case Bindings::ResizeObserverBoxOptions::ContentBox:
+ // Matching sizes are entry.contentBoxSize if observation.observedBox is "content-box"
+ observation->last_reported_sizes() = entry->content_box_size();
+ break;
+ case Bindings::ResizeObserverBoxOptions::DevicePixelContentBox:
+ // Matching sizes are entry.devicePixelContentBoxSize if observation.observedBox is "device-pixel-content-box"
+ observation->last_reported_sizes() = entry->device_pixel_content_box_size();
+ break;
+ default:
+ VERIFY_NOT_REACHED();
+ }
+
+ // 4. Set targetDepth to the result of calculate depth for node for observation.target.
+ auto target_depth = calculate_depth_for_node(*observation->target());
+
+ // 5. Set shallowestTargetDepth to targetDepth if targetDepth < shallowestTargetDepth
+ if (target_depth < shallowest_target_depth)
+ shallowest_target_depth = target_depth;
+ }
+
+ // 4. Invoke observer.[[callback]] with entries.
+ observer->invoke_callback(entries);
+
+ // 5. Clear observer.[[activeTargets]].
+ observer->active_targets().clear();
+ }
+
+ return shallowest_target_depth;
+}
+
+// https://drafts.csswg.org/resize-observer-1/#has-active-observations-h
+bool Document::has_active_resize_observations()
+{
+ // 1. For each observer in [[resizeObservers]] run this step:
+ for (auto const& observer : m_resize_observers) {
+ // 1. If observer.[[activeTargets]] is not empty, return true.
+ if (!observer->active_targets().is_empty())
+ return true;
+ }
+
+ // 2. Return false.
+ return false;
+}
+
+// https://drafts.csswg.org/resize-observer-1/#has-skipped-observations-h
+bool Document::has_skipped_resize_observations()
+{
+ // 1. For each observer in [[resizeObservers]] run this step:
+ for (auto const& observer : m_resize_observers) {
+ // 1. If observer.[[skippedTargets]] is not empty, return true.
+ if (!observer->skipped_targets().is_empty())
+ return true;
+ }
+
+ // 2. Return false.
+ return false;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h
index 55a040d5a8..a994c65502 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.h
+++ b/Userland/Libraries/LibWeb/DOM/Document.h
@@ -590,6 +590,11 @@ public:
virtual Vector supported_property_names() const override;
Vector> const& potentially_named_elements() const { return m_potentially_named_elements; }
+ void gather_active_observations_at_depth(size_t depth);
+ [[nodiscard]] size_t broadcast_active_resize_observations();
+ [[nodiscard]] bool has_active_resize_observations();
+ [[nodiscard]] bool has_skipped_resize_observations();
+
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp
index 923be22bca..a4ae628dee 100644
--- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp
+++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp
@@ -207,6 +207,45 @@ void EventLoop::process()
run_animation_frame_callbacks(document, now);
});
+ // FIXME: This step is implemented following the latest specification, while the rest of this method uses an outdated spec.
+ // NOTE: Gathering and broadcasting of resize observations need to happen after evaluating media queries but before
+ // updating intersection observations steps.
+ for_each_fully_active_document_in_docs([&](DOM::Document& document) {
+ // 1. Let resizeObserverDepth be 0.
+ size_t resize_observer_depth = 0;
+
+ // 2. While true:
+ while (true) {
+ // 1. Recalculate styles and update layout for doc.
+ // NOTE: Recalculation of styles is handled by update_layout()
+ document.update_layout();
+
+ // FIXME: 2. Let hadInitialVisibleContentVisibilityDetermination be false.
+ // FIXME: 3. For each element element with 'auto' used value of 'content-visibility':
+ // FIXME: 4. If hadInitialVisibleContentVisibilityDetermination is true, then continue.
+
+ // 5. Gather active resize observations at depth resizeObserverDepth for doc.
+ document.gather_active_observations_at_depth(resize_observer_depth);
+
+ // 6. If doc has active resize observations:
+ if (document.has_active_resize_observations()) {
+ // 1. Set resizeObserverDepth to the result of broadcasting active resize observations given doc.
+ resize_observer_depth = document.broadcast_active_resize_observations();
+
+ // 2. Continue.
+ continue;
+ }
+
+ // 7. Otherwise, break.
+ break;
+ }
+
+ // 3. If doc has skipped resize observations, then deliver resize loop error given doc.
+ if (document.has_skipped_resize_observations()) {
+ // FIXME: Deliver resize loop error.
+ }
+ });
+
// 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
for_each_fully_active_document_in_docs([&](DOM::Document& document) {
document.run_the_update_intersection_observations_steps(now);
diff --git a/Userland/Libraries/LibWeb/HTML/Navigable.cpp b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
index 1860354d61..e9f73bed96 100644
--- a/Userland/Libraries/LibWeb/HTML/Navigable.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
@@ -2093,7 +2093,6 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig
auto viewport_rect = page.css_to_device_rect(this->viewport_rect());
Gfx::IntRect bitmap_rect { {}, viewport_rect.size().to_type() };
- document->update_layout();
auto background_color = document->background_color();
recording_painter.fill_rect(bitmap_rect, background_color);