diff --git a/packages/react-dom-bindings/src/events/SyntheticEvent.js b/packages/react-dom-bindings/src/events/SyntheticEvent.js index c11c994268a6a..a0ad6cea6ed0d 100644 --- a/packages/react-dom-bindings/src/events/SyntheticEvent.js +++ b/packages/react-dom-bindings/src/events/SyntheticEvent.js @@ -592,3 +592,11 @@ const WheelEventInterface = { }; export const SyntheticWheelEvent: $FlowFixMe = createSyntheticEvent(WheelEventInterface); + +const ToggleEventInterface = { + ...EventInterface, + newState: '', + oldState: '', +}; +export const SyntheticToggleEvent: EventInterfaceType = + createSyntheticEvent(ToggleEventInterface); diff --git a/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js index f4538bc83617c..8c983b3f5dfcc 100644 --- a/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js @@ -27,6 +27,7 @@ import { SyntheticWheelEvent, SyntheticClipboardEvent, SyntheticPointerEvent, + SyntheticToggleEvent, } from '../../events/SyntheticEvent'; import { @@ -161,6 +162,11 @@ function extractEvents( case 'pointerup': SyntheticEventCtor = SyntheticPointerEvent; break; + case 'toggle': + case 'beforetoggle': + // MDN claims
should not receive ToggleEvent contradicting the spec: https://html.spec.whatwg.org/multipage/indices.html#event-toggle + SyntheticEventCtor = SyntheticToggleEvent; + break; default: // Unknown event. This is used by createEventHandle. break; diff --git a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js index 4cd9c8f27e6f5..afc8cadf9d6ca 100644 --- a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js +++ b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js @@ -9,6 +9,15 @@ 'use strict'; +// polyfill missing JSDOM support +class ToggleEvent extends Event { + constructor(type, eventInit) { + super(type, eventInit); + this.newState = eventInit.newState; + this.oldState = eventInit.oldState; + } +} + describe('SimpleEventPlugin', function () { let React; let ReactDOMClient; @@ -469,5 +478,116 @@ describe('SimpleEventPlugin', function () { 'wheel', ]); }); + + it('dispatches synthetic toggle events when the Popover API is used', async () => { + container = document.createElement('div'); + + const onToggle = jest.fn(); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + <> + +
+ popover content +
+ , + ); + }); + + const target = container.querySelector('#popover'); + target.dispatchEvent( + new ToggleEvent('toggle', { + bubbles: false, + cancelable: true, + oldState: 'closed', + newState: 'open', + }), + ); + + expect(onToggle).toHaveBeenCalledTimes(1); + let event = onToggle.mock.calls[0][0]; + expect(event).toEqual( + expect.objectContaining({ + oldState: 'closed', + newState: 'open', + }), + ); + + target.dispatchEvent( + new ToggleEvent('toggle', { + bubbles: false, + cancelable: true, + oldState: 'open', + newState: 'closed', + }), + ); + + expect(onToggle).toHaveBeenCalledTimes(2); + event = onToggle.mock.calls[1][0]; + expect(event).toEqual( + expect.objectContaining({ + oldState: 'open', + newState: 'closed', + }), + ); + }); + + it('dispatches synthetic toggle events when
is used', async () => { + // This test just replays browser behavior. + // The real test would be if browsers dispatch ToggleEvent on
. + // This case only exists because MDN claims
doesn't receive ToggleEvent. + // However, Chrome dispatches ToggleEvent on
and the spec confirms that behavior: https://html.spec.whatwg.org/multipage/indices.html#event-toggle + + container = document.createElement('div'); + + const onToggle = jest.fn(); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ Summary + Details +
, + ); + }); + + const target = container.querySelector('#details'); + target.dispatchEvent( + new ToggleEvent('toggle', { + bubbles: false, + cancelable: true, + oldState: 'closed', + newState: 'open', + }), + ); + + expect(onToggle).toHaveBeenCalledTimes(1); + let event = onToggle.mock.calls[0][0]; + expect(event).toEqual( + expect.objectContaining({ + oldState: 'closed', + newState: 'open', + }), + ); + + target.dispatchEvent( + new ToggleEvent('toggle', { + bubbles: false, + cancelable: true, + oldState: 'open', + newState: 'closed', + }), + ); + + expect(onToggle).toHaveBeenCalledTimes(2); + event = onToggle.mock.calls[1][0]; + expect(event).toEqual( + expect.objectContaining({ + oldState: 'open', + newState: 'closed', + }), + ); + }); }); });