Skip to content

Commit

Permalink
Bug 1876762 - Implement togggle events for Dialog show/showModal/clos…
Browse files Browse the repository at this point in the history
…e r=smaug

whatwg/html#10091

This also slightly refactors FireToggleEvent to allow to to
accomodate both popovers and now also dialogs

Differential Revision: https://phabricator.services.mozilla.com/D225449
  • Loading branch information
Keith Cirkel committed Oct 14, 2024
1 parent 84bcf1a commit f0551e9
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 36 deletions.
3 changes: 1 addition & 2 deletions dom/base/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15563,8 +15563,7 @@ void Document::HidePopover(Element& aPopover, bool aFocusPreviousElement,
if (fireEvents) {
// Intentionally ignore the return value here as only on open event for
// beforetoggle the cancelable attribute is initialized to true.
popoverHTMLEl->FireToggleEvent(PopoverVisibilityState::Showing,
PopoverVisibilityState::Hidden,
popoverHTMLEl->FireToggleEvent(u"open"_ns, u"closed"_ns,
u"beforetoggle"_ns);

// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm
Expand Down
52 changes: 52 additions & 0 deletions dom/html/HTMLDialogElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ void HTMLDialogElement::Close(
if (!Open()) {
return;
}

if (StaticPrefs::dom_element_dialog_toggle_events_enabled()) {
FireToggleEvent(u"open"_ns, u"closed"_ns, u"beforetoggle"_ns);
if (!Open()) {
return;
}
QueueToggleEventTask();
}

if (aReturnValue.WasPassed()) {
SetReturnValue(aReturnValue.Value());
}
Expand Down Expand Up @@ -104,6 +113,16 @@ void HTMLDialogElement::Show(ErrorResult& aError) {
"Cannot call show() on an open modal dialog.");
}

if (StaticPrefs::dom_element_dialog_toggle_events_enabled()) {
if (FireToggleEvent(u"closed"_ns, u"open"_ns, u"beforetoggle"_ns)) {
return;
}
if (Open()) {
return;
}
QueueToggleEventTask();
}

SetOpen(true, IgnoreErrors());

StorePreviouslyFocusedElement();
Expand Down Expand Up @@ -180,6 +199,16 @@ void HTMLDialogElement::ShowModal(ErrorResult& aError) {
"Dialog element is already an open popover.");
}

if (StaticPrefs::dom_element_dialog_toggle_events_enabled()) {
if (FireToggleEvent(u"closed"_ns, u"open"_ns, u"beforetoggle"_ns)) {
return;
}
if (Open()) {
return;
}
QueueToggleEventTask();
}

AddToTopLayerIfNeeded();

SetOpen(true, aError);
Expand Down Expand Up @@ -215,6 +244,12 @@ void HTMLDialogElement::ShowModal(ErrorResult& aError) {
aError.SuppressException();
}

void HTMLDialogElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
if (mToggleEventDispatcher == aEvent) {
mToggleEventDispatcher = nullptr;
}
}

void HTMLDialogElement::FocusDialog() {
// 1) If subject is inert, return.
// 2) Let control be the first descendant element of subject, in tree
Expand Down Expand Up @@ -294,6 +329,23 @@ bool HTMLDialogElement::HandleInvokeInternal(Element* aInvoker,
return false;
}

void HTMLDialogElement::QueueToggleEventTask() {
nsAutoString oldState;
auto newState = Open() ? u"closed"_ns : u"open"_ns;
if (mToggleEventDispatcher) {
oldState.Truncate();
static_cast<ToggleEvent*>(mToggleEventDispatcher->mEvent.get())
->GetOldState(oldState);
mToggleEventDispatcher->Cancel();
} else {
oldState.Assign(Open() ? u"open"_ns : u"closed"_ns);
}
RefPtr<ToggleEvent> toggleEvent =
CreateToggleEvent(u"toggle"_ns, oldState, newState, Cancelable::eNo);
mToggleEventDispatcher = new AsyncEventDispatcher(this, toggleEvent.forget());
mToggleEventDispatcher->PostDOMEvent();
}

JSObject* HTMLDialogElement::WrapNode(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return HTMLDialogElement_Binding::Wrap(aCx, this, aGivenProto);
Expand Down
10 changes: 8 additions & 2 deletions dom/html/HTMLDialogElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ class HTMLDialogElement final : public nsGenericHTMLElement {

void UnbindFromTree(UnbindContext&) override;

void Close(const mozilla::dom::Optional<nsAString>& aReturnValue);
MOZ_CAN_RUN_SCRIPT_BOUNDARY void Close(
const mozilla::dom::Optional<nsAString>& aReturnValue);
MOZ_CAN_RUN_SCRIPT void Show(ErrorResult& aError);
MOZ_CAN_RUN_SCRIPT void ShowModal(ErrorResult& aError);

void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;

bool IsInTopLayer() const;
void QueueCancelDialog();
void RunCancelDialogSteps();
MOZ_CAN_RUN_SCRIPT void RunCancelDialogSteps();

MOZ_CAN_RUN_SCRIPT_BOUNDARY void FocusDialog();

Expand All @@ -66,9 +69,12 @@ class HTMLDialogElement final : public nsGenericHTMLElement {
void AddToTopLayerIfNeeded();
void RemoveFromTopLayerIfNeeded();
void StorePreviouslyFocusedElement();
MOZ_CAN_RUN_SCRIPT_BOUNDARY void QueueToggleEventTask();

nsWeakPtr mPreviouslyFocusedElement;

RefPtr<AsyncEventDispatcher> mToggleEventDispatcher;

// This won't need to be cycle collected as CloseWatcher only has strong
// references to event listeners, which themselves have Weak References back
// to the Node.
Expand Down
23 changes: 11 additions & 12 deletions dom/html/nsGenericHTMLElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3408,18 +3408,13 @@ already_AddRefed<ToggleEvent> nsGenericHTMLElement::CreateToggleEvent(
return event.forget();
}

bool nsGenericHTMLElement::FireToggleEvent(PopoverVisibilityState aOldState,
PopoverVisibilityState aNewState,
bool nsGenericHTMLElement::FireToggleEvent(const nsAString& aOldState,
const nsAString& aNewState,
const nsAString& aType) {
auto stringForState = [](PopoverVisibilityState state) {
return state == PopoverVisibilityState::Hidden ? u"closed"_ns : u"open"_ns;
};
const auto cancelable = aType == u"beforetoggle"_ns &&
aNewState == PopoverVisibilityState::Showing
const auto cancelable = aType == u"beforetoggle"_ns && aNewState == u"open"_ns
? Cancelable::eYes
: Cancelable::eNo;
RefPtr event = CreateToggleEvent(aType, stringForState(aOldState),
stringForState(aNewState), cancelable);
RefPtr event = CreateToggleEvent(aType, aOldState, aNewState, cancelable);
EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr);
return event->DefaultPrevented();
}
Expand Down Expand Up @@ -3454,7 +3449,12 @@ void nsGenericHTMLElement::RunPopoverToggleEventTask(
data->ClearToggleEventTask();
// Intentionally ignore the return value here as only on open event the
// cancelable attribute is initialized to true for beforetoggle event.
FireToggleEvent(aOldState, data->GetPopoverVisibilityState(), u"toggle"_ns);
auto stringForState = [](PopoverVisibilityState state) {
return state == PopoverVisibilityState::Hidden ? u"closed"_ns : u"open"_ns;
};
FireToggleEvent(stringForState(aOldState),
stringForState(data->GetPopoverVisibilityState()),
u"toggle"_ns);
}

// https://html.spec.whatwg.org/#dom-showpopover
Expand All @@ -3480,8 +3480,7 @@ void nsGenericHTMLElement::ShowPopoverInternal(Element* aInvoker,
});

// Fire beforetoggle event and re-check popover validity.
if (FireToggleEvent(PopoverVisibilityState::Hidden,
PopoverVisibilityState::Showing, u"beforetoggle"_ns)) {
if (FireToggleEvent(u"closed"_ns, u"open"_ns, u"beforetoggle"_ns)) {
return;
}
if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) {
Expand Down
6 changes: 3 additions & 3 deletions dom/html/nsGenericHTMLElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase {
const nsAString& aEventType, const nsAString& aOldState,
const nsAString& aNewState, mozilla::Cancelable);
/** Returns true if the event has been cancelled. */
MOZ_CAN_RUN_SCRIPT bool FireToggleEvent(
mozilla::dom::PopoverVisibilityState aOldState,
mozilla::dom::PopoverVisibilityState aNewState, const nsAString& aType);
MOZ_CAN_RUN_SCRIPT bool FireToggleEvent(const nsAString& aOldState,
const nsAString& aNewState,
const nsAString& aType);
MOZ_CAN_RUN_SCRIPT void QueuePopoverEventTask(
mozilla::dom::PopoverVisibilityState aOldState);
MOZ_CAN_RUN_SCRIPT void RunPopoverToggleEventTask(
Expand Down
7 changes: 7 additions & 0 deletions modules/libpref/init/StaticPrefList.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,13 @@
value: @IS_NIGHTLY_BUILD@
mirror: always

# Whether Dialog elements emit beforetoggle and toggle events during opening and closing.
# See https://github.com/whatwg/html/pull/10091
- name: dom.element.dialog.toggle_events.enabled
type: bool
value: true
mirror: always

- name: dom.element.transform-getters.enabled
type: bool
value: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,2 @@
[toggle-events.tentative.html]
[dialog.show() should fire beforetoggle and toggle events.]
expected: FAIL

[dialog.show() should fire cancelable beforetoggle which does not open dialog if canceled]
expected: FAIL

[dialog.show() should coalesce asynchronous toggle events.]
expected: FAIL

[dialog.showModal() should fire beforetoggle and toggle events.]
expected: FAIL

[dialog.showModal() should fire cancelable beforetoggle which does not open dialog if canceled]
expected: FAIL

[dialog.showModal() should coalesce asynchronous toggle events.]
expected: FAIL
prefs: [dom.element.dialog.toggle_events.enabled:true]
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,36 @@
mydialog.close();
await waitForTick();
}, `dialog.${methodName}() should coalesce asynchronous toggle events.`);

promise_test(async (t) => {
let attributeChanges = 0;
const mo = new MutationObserver((records) => {
attributeChanges += records.length;
});
mo.observe(mydialog, { attributeFilter: ['open'] });
t.add_cleanup(() => {
mo.disconnect();
});
mydialog.addEventListener("beforetoggle", () => {
mydialog[methodName]();
}, { once: true });

mydialog[methodName]();
assert_true(mydialog.open, "Dialog is open");
await waitForTick();
mo.takeRecords();
assert_equals(attributeChanges, 1, "Should have set open once");

attributeChanges = 0;
mydialog.addEventListener("beforetoggle", () => {
mydialog.close();
}, { once: true });

mydialog.close();
assert_false(mydialog.open, "Dialog is closed");
await waitForTick();
mo.takeRecords();
assert_equals(attributeChanges, 1, "Should have removed open once");
}, `dialog.${methodName}() should not double-set open/close if beforetoggle re-opens`);
});
</script>

0 comments on commit f0551e9

Please sign in to comment.