diff --git a/Libraries/LibWeb/HTML/AttributeNames.h b/Libraries/LibWeb/HTML/AttributeNames.h index 469d3c9e816..ff0774c6164 100644 --- a/Libraries/LibWeb/HTML/AttributeNames.h +++ b/Libraries/LibWeb/HTML/AttributeNames.h @@ -45,6 +45,7 @@ namespace AttributeNames { __ENUMERATE_HTML_ATTRIBUTE(class_) \ __ENUMERATE_HTML_ATTRIBUTE(classid) \ __ENUMERATE_HTML_ATTRIBUTE(clear) \ + __ENUMERATE_HTML_ATTRIBUTE(closedby) \ __ENUMERATE_HTML_ATTRIBUTE(code) \ __ENUMERATE_HTML_ATTRIBUTE(codebase) \ __ENUMERATE_HTML_ATTRIBUTE(codetype) \ diff --git a/Libraries/LibWeb/HTML/CloseWatcher.cpp b/Libraries/LibWeb/HTML/CloseWatcher.cpp index 9e42ce5ea41..4ef0cf6fa9e 100644 --- a/Libraries/LibWeb/HTML/CloseWatcher.cpp +++ b/Libraries/LibWeb/HTML/CloseWatcher.cpp @@ -86,28 +86,32 @@ bool CloseWatcher::request_close() if (!m_is_active) return true; - // 2. If closeWatcher's is running cancel action is true, then return true. + // 2. If the result of running closeWatcher's get enabled state is false, then return true. + if (!get_enabled_state()) + return true; + + // 3. If closeWatcher's is running cancel action is true, then return true. if (m_is_running_cancel_action) return true; - // 3. Let window be closeWatcher's window. + // 4. Let window be closeWatcher's window. auto& window = verify_cast(realm().global_object()); - // 4. If window's associated Document is not fully active, then return true. + // 5. If window's associated Document is not fully active, then return true. if (!window.associated_document().is_fully_active()) return true; - // 5. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups, + // 6. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups, // and window has history-action activation; otherwise false. auto manager = window.close_watcher_manager(); bool can_prevent_close = manager->can_prevent_close() && window.has_history_action_activation(); - // 6. Set closeWatcher's is running cancel action to true. + // 7. Set closeWatcher's is running cancel action to true. m_is_running_cancel_action = true; - // 7. Let shouldContinue be the result of running closeWatcher's cancel action given canPreventClose. + // 8. Let shouldContinue be the result of running closeWatcher's cancel action given canPreventClose. bool should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close })); - // 8. Set closeWatcher's is running cancel action to false. + // 9. Set closeWatcher's is running cancel action to false. m_is_running_cancel_action = false; - // 9. If shouldContinue is false, then: + // 10. If shouldContinue is false, then: if (!should_continue) { // 9.1 Assert: canPreventClose is true. VERIFY(can_prevent_close); @@ -116,10 +120,10 @@ bool CloseWatcher::request_close() return false; } - // 10. Close closeWatcher. + // 11. Close closeWatcher. close(); - // 11. Return true. + // 12. Return true. return true; } @@ -130,14 +134,18 @@ void CloseWatcher::close() if (!m_is_active) return; - // 2. If closeWatcher's window's associated Document is not fully active, then return. + // 2. If the result of running closeWatcher's get enabled state is false, then return. + if (!get_enabled_state()) + return; + + // 3. If closeWatcher's window's associated Document is not fully active, then return. if (!verify_cast(realm().global_object()).associated_document().is_fully_active()) return; - // 3. Destroy closeWatcher. + // 4. Destroy closeWatcher. destroy(); - // 4. Run closeWatcher's close action. + // 5. Run closeWatcher's close action. dispatch_event(DOM::Event::create(realm(), HTML::EventNames::close)); } diff --git a/Libraries/LibWeb/HTML/CloseWatcher.h b/Libraries/LibWeb/HTML/CloseWatcher.h index 6a83d734c5a..24ee4245242 100644 --- a/Libraries/LibWeb/HTML/CloseWatcher.h +++ b/Libraries/LibWeb/HTML/CloseWatcher.h @@ -37,6 +37,9 @@ public: void set_onclose(WebIDL::CallbackType*); WebIDL::CallbackType* onclose(); + bool get_enabled_state() const { return m_enabled_state; } + void set_enabled_state(bool enabled) { m_enabled_state = enabled; } + private: CloseWatcher(JS::Realm&); @@ -44,6 +47,7 @@ private: bool m_is_running_cancel_action { false }; bool m_is_active { true }; + bool m_enabled_state { true }; }; } diff --git a/Libraries/LibWeb/HTML/CloseWatcherManager.cpp b/Libraries/LibWeb/HTML/CloseWatcherManager.cpp index bf3ea88ac65..a26f6c9af38 100644 --- a/Libraries/LibWeb/HTML/CloseWatcherManager.cpp +++ b/Libraries/LibWeb/HTML/CloseWatcherManager.cpp @@ -75,11 +75,12 @@ bool CloseWatcherManager::process_close_watchers() } // 2.2 For each closeWatcher of group, in reverse order: for (auto it = group_copy.rbegin(); it != group_copy.rend(); ++it) { - // 2.1.1 Set processedACloseWatcher to true. - processed_a_close_watcher = true; - // 2.1.2 Let shouldProceed be the result of requesting to close closeWatcher. + // 2.2.1 If the result of running closeWatcher's get enabled state is true, then set processedACloseWatcher to true. + if ((*it)->get_enabled_state()) + processed_a_close_watcher = true; + // 2.2.2 Let shouldProceed be the result of requesting to close closeWatcher. bool should_proceed = (*it)->request_close(); - // 2.1.3 If shouldProceed is false, then break; + // 2.2.3 If shouldProceed is false, then break; if (!should_proceed) break; } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 83067ec9d34..84f9af95a0f 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -130,15 +130,18 @@ WebIDL::ExceptionOr HTMLDialogElement::show() // 6. Add an open attribute to this, whose value is the empty string. TRY(set_attribute(AttributeNames::open, {})); - // FIXME: 7. Set this's previously focused element to the focused element. + // 7. Set the dialog close watcher with this. + set_the_dialog_close_watcher(); - // FIXME: 8. Let hideUntil be the result of running topmost popover ancestor given this, null, and false. + // FIXME: 8. Set this's previously focused element to the focused element. - // FIXME: 9. If hideUntil is null, then set hideUntil to this's node document. + // FIXME: 9. Let hideUntil be the result of running topmost popover ancestor given this, null, and false. - // FIXME: 10. Run hide all popovers given this's node document. + // FIXME: 10. If hideUntil is null, then set hideUntil to this's node document. - // 11. Run the dialog focusing steps given this. + // FIXME: 11. Run hide all popovers given this's node document. + + // 12. Run the dialog focusing steps given this. run_dialog_focusing_steps(); return {}; @@ -206,31 +209,8 @@ WebIDL::ExceptionOr HTMLDialogElement::show_modal() if (!document().top_layer_elements().contains(*this)) document().add_an_element_to_the_top_layer(*this); - // 15. Set this's close watcher to the result of establishing a close watcher given this's relevant global object, with: - m_close_watcher = CloseWatcher::establish(*document().window()); - // - cancelAction given canPreventClose being to return the result of firing an event named cancel at this, with the cancelable attribute initialized to canPreventClose. - auto cancel_callback_function = JS::NativeFunction::create( - realm(), [this](JS::VM& vm) { - auto& event = verify_cast(vm.argument(0).as_object()); - bool can_prevent_close = event.cancelable(); - auto should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close })); - if (!should_continue) - event.prevent_default(); - return JS::js_undefined(); - }, - 0, "", &realm()); - auto cancel_callback = realm().heap().allocate(*cancel_callback_function, realm()); - m_close_watcher->add_event_listener_without_options(HTML::EventNames::cancel, DOM::IDLEventListener::create(realm(), cancel_callback)); - // - closeAction being to close the dialog given this and null. - auto close_callback_function = JS::NativeFunction::create( - realm(), [this](JS::VM&) { - close_the_dialog({}); - - return JS::js_undefined(); - }, - 0, "", &realm()); - auto close_callback = realm().heap().allocate(*close_callback_function, realm()); - m_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback)); + // 15. Set the dialog close watcher with this. + set_the_dialog_close_watcher(); // FIXME: 16. Set this's previously focused element to the focused element. @@ -254,6 +234,25 @@ void HTMLDialogElement::close(Optional return_value) close_the_dialog(move(return_value)); } +WebIDL::ExceptionOr HTMLDialogElement::request_close(Optional return_value) +{ + // 1. If this does not have an open attribute, then return. + if (!has_attribute(AttributeNames::open)) + return {}; + // 2. If this's computed closed-by state is None, then throw an "InvalidStateError" DOMException. + if (closed_by() == "none") + return WebIDL::InvalidStateError::create(realm(), ""_string); + // 3. Assert: this's close watcher is not null. + VERIFY(m_close_watcher); + // 4. If returnValue is not given, then set it to null. + // 5. Set this's request close return value to returnValue. + m_request_close_return_value = move(return_value); + // 6. Request to close dialog's close watcher. + m_close_watcher->request_close(); + + return {}; +} + // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-returnvalue String HTMLDialogElement::return_value() const { @@ -266,6 +265,21 @@ void HTMLDialogElement::set_return_value(String return_value) m_return_value = move(return_value); } +String HTMLDialogElement::closed_by() const +{ + auto value = get_attribute(HTML::AttributeNames::closedby); + + if (value.has_value() && (value.value() == "none" || value.value() == "closerequest" || value.value() == "any")) + return value.value(); + + return m_is_modal ? "closerequest"_string : "none"_string; +} + +WebIDL::ExceptionOr HTMLDialogElement::set_closed_by(String value) +{ + return set_attribute(HTML::AttributeNames::closedby, value); +} + // https://html.spec.whatwg.org/multipage/interactive-elements.html#close-the-dialog void HTMLDialogElement::close_the_dialog(Optional result) { @@ -303,19 +317,22 @@ void HTMLDialogElement::close_the_dialog(Optional result) if (result.has_value()) set_return_value(result.release_value()); - // FIXME: 10. If subject's previously focused element is not null, then: + // 10. Set the request close return value to null. + m_request_close_return_value = {}; + + // FIXME: 11. If subject's previously focused element is not null, then: // 1. Let element be subject's previously focused element. // 2. Set subject's previously focused element to null. // 3. If subject's node document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of element, // or wasModal is true, then run the focusing steps for element; the viewport should not be scrolled by doing this step. - // 11. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject. + // 12. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject. queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { auto close_event = DOM::Event::create(realm(), HTML::EventNames::close); dispatch_event(close_event); }); - // 12. If subject's close watcher is not null, then: + // 13. If subject's close watcher is not null, then: if (m_close_watcher) { // 9.1 Destroy subject's close watcher. m_close_watcher->destroy(); @@ -324,6 +341,37 @@ void HTMLDialogElement::close_the_dialog(Optional result) } } +void HTMLDialogElement::set_the_dialog_close_watcher() +{ + // 1. Set dialog's close watcher to the result of establishing a close watcher given dialog's relevant global object, with: + m_close_watcher = CloseWatcher::establish(*document().window()); + // - cancelAction given canPreventClose being to return the result of firing an event named cancel at dialog, with the cancelable attribute initialized to canPreventClose. + auto cancel_callback_function = JS::NativeFunction::create( + realm(), [this](JS::VM& vm) { + auto& event = verify_cast(vm.argument(0).as_object()); + bool can_prevent_close = event.cancelable(); + auto should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close })); + if (!should_continue) + event.prevent_default(); + return JS::js_undefined(); + }, + 0, "", &realm()); + auto cancel_callback = realm().heap().allocate(*cancel_callback_function, realm()); + m_close_watcher->add_event_listener_without_options(HTML::EventNames::cancel, DOM::IDLEventListener::create(realm(), cancel_callback)); + // - closeAction being to close the dialog given dialog and dialog's request close return value. + auto close_callback_function = JS::NativeFunction::create( + realm(), [this](JS::VM&) { + close_the_dialog(m_request_close_return_value); + + return JS::js_undefined(); + }, + 0, "", &realm()); + auto close_callback = realm().heap().allocate(*close_callback_function, realm()); + m_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback)); + // - getEnabledState being to return true if dialog's computed closed-by state is not None; otherwise false. + m_close_watcher->set_enabled_state(closed_by() != "none"); +} + // https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps void HTMLDialogElement::run_dialog_focusing_steps() { @@ -341,4 +389,12 @@ void HTMLDialogElement::run_dialog_focusing_steps() run_focusing_steps(control); } +void HTMLDialogElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) +{ + Base::attribute_changed(name, old_value, value, namespace_); + + if (name == HTML::AttributeNames::closedby && m_close_watcher && old_value != value) + m_close_watcher->set_enabled_state(closed_by() != "none"); +} + } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.h b/Libraries/LibWeb/HTML/HTMLDialogElement.h index 3d910ee4949..7623b49624e 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.h +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.h @@ -25,9 +25,13 @@ public: String return_value() const; void set_return_value(String); + String closed_by() const; + WebIDL::ExceptionOr set_closed_by(String); + WebIDL::ExceptionOr show(); WebIDL::ExceptionOr show_modal(); void close(Optional return_value); + WebIDL::ExceptionOr request_close(Optional return_value); // https://www.w3.org/TR/html-aria/#el-dialog virtual Optional default_role() const override { return ARIA::Role::dialog; } @@ -40,15 +44,21 @@ private: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + // ^DOM::Element + virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; + void queue_a_dialog_toggle_event_task(String old_state, String new_state); void close_the_dialog(Optional result); void run_dialog_focusing_steps(); + void set_the_dialog_close_watcher(); + String m_return_value; bool m_is_modal { false }; GC::Ptr m_close_watcher; + Optional m_request_close_return_value; // https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-toggle-task-tracker Optional m_dialog_toggle_task_tracker; diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.idl b/Libraries/LibWeb/HTML/HTMLDialogElement.idl index f83f8f8ae07..61c594632d5 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.idl +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.idl @@ -8,8 +8,10 @@ interface HTMLDialogElement : HTMLElement { [CEReactions, Reflect] attribute boolean open; attribute DOMString returnValue; + [CEReactions] attribute DOMString closedBy; [CEReactions] undefined show(); [CEReactions] undefined showModal(); [CEReactions] undefined close(optional DOMString returnValue); + [CEReactions] undefined requestClose(optional DOMString returnValue); };