Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Implement dialog.requestClose() #3280

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions Libraries/LibWeb/HTML/CloseWatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,47 +79,59 @@ CloseWatcher::CloseWatcher(JS::Realm& realm)
{
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-closewatcher-requestclose
void CloseWatcher::request_close()
{
// The requestClose() method steps are to request to close this's internal close watcher with false.
request_close_internal(false);
}

// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-request-close
bool CloseWatcher::request_close()
bool CloseWatcher::request_close_internal(bool require_history_action_activation)
{
// 1. If closeWatcher is not active, then return.
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<HTML::Window>(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 requireHistoryActionActivation is false, or 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.
bool can_prevent_close = !require_history_action_activation || (manager->can_prevent_close() && window.has_history_action_activation());
// 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.
// 10.1 Assert: canPreventClose is true.
VERIFY(can_prevent_close);
// 9.2 Consume history-action user activation given window.
// 10.2 Consume history-action user activation given window.
window.consume_history_action_user_activation();
// 10.3 Return false.
return false;
}

// 10. Close closeWatcher.
// 11. Close closeWatcher.
close();

// 11. Return true.
// 12. Return true.
return true;
}

Expand All @@ -130,14 +142,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<HTML::Window>(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));
}

Expand Down
8 changes: 7 additions & 1 deletion Libraries/LibWeb/HTML/CloseWatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ class CloseWatcher final : public DOM::EventTarget {
static WebIDL::ExceptionOr<GC::Ref<CloseWatcher>> construct_impl(JS::Realm&, CloseWatcherOptions const& = {});
[[nodiscard]] static GC::Ref<CloseWatcher> establish(HTML::Window&);

bool request_close();
void request_close();
void close();
void destroy();

bool request_close_internal(bool require_history_action_activation);

bool get_enabled_state() const { return m_is_enabled; }
void set_enabled(bool enabled) { m_is_enabled = enabled; }

virtual ~CloseWatcher() override = default;

void set_oncancel(WebIDL::CallbackType*);
Expand All @@ -44,6 +49,7 @@ class CloseWatcher final : public DOM::EventTarget {

bool m_is_running_cancel_action { false };
bool m_is_active { true };
bool m_is_enabled { true };
};

}
11 changes: 6 additions & 5 deletions Libraries/LibWeb/HTML/CloseWatcherManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
bool should_proceed = (*it)->request_close();
// 2.1.3 If shouldProceed is false, then break;
// 2.2.1 If the result of running closeWatcher's get enabled state is true, 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 with true.
bool should_proceed = (*it)->request_close_internal(true);
// 2.2.3 If shouldProceed is false, then break;
if (!should_proceed)
break;
}
Expand Down
138 changes: 98 additions & 40 deletions Libraries/LibWeb/HTML/HTMLDialogElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,26 @@ WebIDL::ExceptionOr<void> 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.
// FIXME: ADHOC see (https://github.com/whatwg/html/issues/10921): Assert: this's node document's open dialog list does not contain this.

// FIXME: 8. Let hideUntil be the result of running topmost popover ancestor given this, null, and false.
// FIXME: 7. Add this to this's node document's open dialogs list.

// FIXME: 9. If hideUntil is null, then set hideUntil to this's node document.
// 8. Set the dialog close watcher with this.
set_close_watcher();

// FIXME: 10. Run hide all popovers given this's node document.
// FIXME: 9. Set this's previously focused element to the focused element.

// 11. Run the dialog focusing steps given this.
// FIXME: 10. Let document be this's node document.

// FIXME: 11. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false.

// FIXME: 12. Let hideUntil be the result of running topmost popover ancestor given this, document's showing auto popover list, null, and false.

// FIXME: 13. If hideUntil is null, then set hideUntil to document.

// FIXME: 14. Run hide all popovers until given hideUntil, false, and true.

// 15. Run the dialog focusing steps given this.
run_dialog_focusing_steps();

return {};
Expand Down Expand Up @@ -200,47 +211,32 @@ WebIDL::ExceptionOr<void> HTMLDialogElement::show_modal()
// 12. Set the is modal flag of this to true.
m_is_modal = true;

// FIXME: 13. Let this's node document be blocked by the modal dialog this.
// FIXME: 13. Assert: this's node document's open dialog list does not contain this.

// FIXME: 14. Add this to this's node document's open dialogs list.

// 14. If this's node document's top layer does not already contain this, then add an element to the top layer given this.
// FIXME: 15. Let this's node document be blocked by the modal dialog this.

// 16. If this's node document's top layer does not already contain this, then add an element to the top layer given this.
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<DOM::Event>(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<WebIDL::CallbackType>(*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({});
// 17. Set the dialog close watcher with this.
set_close_watcher();

return JS::js_undefined();
},
0, "", &realm());
auto close_callback = realm().heap().allocate<WebIDL::CallbackType>(*close_callback_function, realm());
m_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback));
// FIXME: 18. Set this's previously focused element to the focused element.

// FIXME: 16. Set this's previously focused element to the focused element.
// FIXME: 19. Let document be this's node document.

// FIXME: 17. Let hideUntil be the result of running topmost popover ancestor given this, null, and false.
// FIXME: 20. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false.

// FIXME: 18. If hideUntil is null, then set hideUntil to this's node document.
// FIXME: 21. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given this, document's showing auto popover list, null, and false.

// FIXME: 19. Run hide all popovers until given hideUntil, false, and true.
// FIXME: 22. If hideUntil is null, then set hideUntil to document.

// 20. Run the dialog focusing steps given this.
// FIXME: 23. Run hide all popovers until given hideUntil, false, and true.

// 24. Run the dialog focusing steps given this.
run_dialog_focusing_steps();

return {};
Expand All @@ -254,6 +250,29 @@ void HTMLDialogElement::close(Optional<String> return_value)
close_the_dialog(move(return_value));
}

// https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-requestclose
void HTMLDialogElement::request_close(Optional<String> return_value)
{
// 1. If this does not have an open attribute, then return.
if (!has_attribute(AttributeNames::open))
return;
// 2. Assert: this's close watcher is not null.
VERIFY(m_close_watcher);
// 3. Set dialog's enable close watcher for requestClose() to true.
// ADHOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex.
m_close_watcher->set_enabled(true);
// 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 = return_value;
// 6. Request to close dialog's close watcher with false.
m_close_watcher->request_close_internal(false);
// 7. Set dialog's enable close watcher for requestClose() to false.
// ADHOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex.
// FIXME: This should be set based on dialog closedby state, when implemented.
if (m_close_watcher)
m_close_watcher->set_enabled(m_is_modal);
}

// https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-returnvalue
String HTMLDialogElement::return_value() const
{
Expand Down Expand Up @@ -299,23 +318,28 @@ void HTMLDialogElement::close_the_dialog(Optional<String> result)
// 8. Set the is modal flag of subject to false.
m_is_modal = false;

// 9. If result is not null, then set the returnValue attribute to result.
// FIXME: 9. Remove subject from subject's node document's open dialogs list.

// 10. If result is not null, then set the returnValue attribute to result.
if (result.has_value())
set_return_value(result.release_value());

// FIXME: 10. If subject's previously focused element is not null, then:
// 11. Set the request close return value to null.
m_request_close_return_value = {};

// FIXME: 12. 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.
// 13. 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:
// 14. If subject's close watcher is not null, then:
if (m_close_watcher) {
// 9.1 Destroy subject's close watcher.
m_close_watcher->destroy();
Expand All @@ -324,6 +348,40 @@ void HTMLDialogElement::close_the_dialog(Optional<String> result)
}
}

// https://html.spec.whatwg.org/multipage/interactive-elements.html#set-the-dialog-close-watcher
void HTMLDialogElement::set_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<DOM::Event>(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<WebIDL::CallbackType>(*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<WebIDL::CallbackType>(*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 enable close watcher for requestClose() is true or dialog's computed closed-by state is not None; otherwise false.
// ADHOC: Implemented slightly differently to the spec, as the spec is unnecessarily complex.
// FIXME: This should be set based on dialog closedby state, when implemented.
m_close_watcher->set_enabled(m_is_modal);
}

// https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps
void HTMLDialogElement::run_dialog_focusing_steps()
{
Expand Down
5 changes: 5 additions & 0 deletions Libraries/LibWeb/HTML/HTMLDialogElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#pragma once

#include <AK/Forward.h>
#include <LibWeb/ARIA/Roles.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/ToggleTaskTracker.h>
Expand All @@ -28,6 +29,7 @@ class HTMLDialogElement final : public HTMLElement {
WebIDL::ExceptionOr<void> show();
WebIDL::ExceptionOr<void> show_modal();
void close(Optional<String> return_value);
void request_close(Optional<String> return_value);

// https://www.w3.org/TR/html-aria/#el-dialog
virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::dialog; }
Expand All @@ -46,8 +48,11 @@ class HTMLDialogElement final : public HTMLElement {

void run_dialog_focusing_steps();

void set_close_watcher();

String m_return_value;
bool m_is_modal { false };
Optional<String> m_request_close_return_value;
GC::Ptr<CloseWatcher> m_close_watcher;

// https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-toggle-task-tracker
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/HTML/HTMLDialogElement.idl
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ interface HTMLDialogElement : HTMLElement {
[CEReactions] undefined show();
[CEReactions] undefined showModal();
[CEReactions] undefined close(optional DOMString returnValue);
[CEReactions] undefined requestClose(optional DOMString returnValue);

};
Loading