diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
index 78db885a46f..969f66e800e 100644
--- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
+++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
@@ -113,6 +113,7 @@ source_set("HTML") {
"HTMLUListElement.cpp",
"HTMLUnknownElement.cpp",
"HTMLVideoElement.cpp",
+ "HashChangeEvent.cpp",
"History.cpp",
"ImageBitmap.cpp",
"ImageData.cpp",
diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni
index aa0c0675547..3b57613c5fa 100644
--- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni
+++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni
@@ -117,6 +117,7 @@ standard_idl_files = [
"//Userland/Libraries/LibWeb/HTML/DataTransfer.idl",
"//Userland/Libraries/LibWeb/HTML/ErrorEvent.idl",
"//Userland/Libraries/LibWeb/HTML/FormDataEvent.idl",
+ "//Userland/Libraries/LibWeb/HTML/HashChangeEvent.idl",
"//Userland/Libraries/LibWeb/HTML/History.idl",
"//Userland/Libraries/LibWeb/HTML/HTMLAllCollection.idl",
"//Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.idl",
diff --git a/Tests/LibWeb/Text/data/iframe-hashchange-event.html b/Tests/LibWeb/Text/data/iframe-hashchange-event.html
new file mode 100644
index 00000000000..2a82ff445cd
--- /dev/null
+++ b/Tests/LibWeb/Text/data/iframe-hashchange-event.html
@@ -0,0 +1,12 @@
+
+
diff --git a/Tests/LibWeb/Text/expected/navigation/hashchange-event.txt b/Tests/LibWeb/Text/expected/navigation/hashchange-event.txt
new file mode 100644
index 00000000000..413ed9a430f
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/navigation/hashchange-event.txt
@@ -0,0 +1,2 @@
+ hashchange oldURL.hash= newURL.hash=#test
+hashchange oldURL.hash=#test newURL.hash=#done
diff --git a/Tests/LibWeb/Text/input/navigation/hashchange-event.html b/Tests/LibWeb/Text/input/navigation/hashchange-event.html
new file mode 100644
index 00000000000..7d92aef3a1f
--- /dev/null
+++ b/Tests/LibWeb/Text/input/navigation/hashchange-event.html
@@ -0,0 +1,13 @@
+
+
+
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index c733043f30e..497868d486a 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -277,6 +277,7 @@ set(SOURCES
HTML/FormControlInfrastructure.cpp
HTML/FormDataEvent.cpp
HTML/GlobalEventHandlers.cpp
+ HTML/HashChangeEvent.cpp
HTML/History.cpp
HTML/HTMLAllCollection.cpp
HTML/HTMLAnchorElement.cpp
diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp
index 99f96d1424a..f7e022b5e5f 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Document.cpp
@@ -79,6 +79,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -4023,10 +4024,19 @@ void Document::update_for_history_step_application(JS::NonnullGCPtrurl().fragment()) {
+ HTML::HashChangeEventInit hashchange_event_init;
+ hashchange_event_init.old_url = MUST(String::from_byte_string(old_url.serialize()));
+ hashchange_event_init.new_url = MUST(String::from_byte_string(entry->url().serialize()));
+ auto hashchange_event = HTML::HashChangeEvent::create(realm(), "hashchange"_fly_string, hashchange_event_init);
+ HTML::queue_global_task(HTML::Task::Source::DOMManipulation, relevant_global_object, [hashchange_event, &relevant_global_object]() {
+ relevant_global_object.dispatch_event(hashchange_event);
+ });
+ }
}
// 6. Otherwise:
diff --git a/Userland/Libraries/LibWeb/HTML/HashChangeEvent.cpp b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.cpp
new file mode 100644
index 00000000000..78e35921a29
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024, Aliaksandr Kalenik
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Web::HTML {
+
+JS_DEFINE_ALLOCATOR(HashChangeEvent);
+
+[[nodiscard]] JS::NonnullGCPtr HashChangeEvent::create(JS::Realm& realm, FlyString const& event_name, HashChangeEventInit const& event_init)
+{
+ return realm.heap().allocate(realm, realm, event_name, event_init);
+}
+
+JS::NonnullGCPtr HashChangeEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, HashChangeEventInit const& event_init)
+{
+ return realm.heap().allocate(realm, realm, event_name, event_init);
+}
+
+HashChangeEvent::HashChangeEvent(JS::Realm& realm, FlyString const& event_name, HashChangeEventInit const& event_init)
+ : DOM::Event(realm, event_name, event_init)
+ , m_old_url(event_init.old_url)
+ , m_new_url(event_init.new_url)
+{
+}
+
+void HashChangeEvent::initialize(JS::Realm& realm)
+{
+ Base::initialize(realm);
+ WEB_SET_PROTOTYPE_FOR_INTERFACE(HashChangeEvent);
+}
+
+void HashChangeEvent::visit_edges(JS::Cell::Visitor& visitor)
+{
+ Base::visit_edges(visitor);
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/HashChangeEvent.h b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.h
new file mode 100644
index 00000000000..2b51cc9b4a1
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2024, Aliaksandr Kalenik
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+
+namespace Web::HTML {
+
+struct HashChangeEventInit : public DOM::EventInit {
+ String old_url;
+ String new_url;
+};
+
+class HashChangeEvent final : public DOM::Event {
+ WEB_PLATFORM_OBJECT(HashChangeEvent, DOM::Event);
+ JS_DECLARE_ALLOCATOR(HashChangeEvent);
+
+public:
+ [[nodiscard]] static JS::NonnullGCPtr create(JS::Realm&, FlyString const& event_name, HashChangeEventInit const&);
+ [[nodiscard]] static JS::NonnullGCPtr construct_impl(JS::Realm&, FlyString const& event_name, HashChangeEventInit const&);
+
+ String old_url() const { return m_old_url; }
+ String new_url() const { return m_new_url; }
+
+private:
+ HashChangeEvent(JS::Realm&, FlyString const& event_name, HashChangeEventInit const& event_init);
+
+ virtual void initialize(JS::Realm&) override;
+ virtual void visit_edges(JS::Cell::Visitor& visitor) override;
+
+ String m_old_url;
+ String m_new_url;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/HashChangeEvent.idl b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.idl
new file mode 100644
index 00000000000..67656dcf4e0
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/HashChangeEvent.idl
@@ -0,0 +1,15 @@
+#import
+
+// https://html.spec.whatwg.org/multipage/nav-history-apis.html#hashchangeevent
+[Exposed=Window]
+interface HashChangeEvent : Event {
+ constructor(DOMString type, optional HashChangeEventInit eventInitDict = {});
+
+ readonly attribute USVString oldURL;
+ readonly attribute USVString newURL;
+};
+
+dictionary HashChangeEventInit : EventInit {
+ USVString oldURL = "";
+ USVString newURL = "";
+};
diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake
index 05f2a9dcbfc..1f9db5188f3 100644
--- a/Userland/Libraries/LibWeb/idl_files.cmake
+++ b/Userland/Libraries/LibWeb/idl_files.cmake
@@ -102,6 +102,7 @@ libweb_js_bindings(HTML/DOMStringMap)
libweb_js_bindings(HTML/DataTransfer)
libweb_js_bindings(HTML/ErrorEvent)
libweb_js_bindings(HTML/FormDataEvent)
+libweb_js_bindings(HTML/HashChangeEvent)
libweb_js_bindings(HTML/History)
libweb_js_bindings(HTML/HTMLAllCollection)
libweb_js_bindings(HTML/HTMLAnchorElement)