diff --git a/fixtures/attribute-behavior/AttributeTableSnapshot.md b/fixtures/attribute-behavior/AttributeTableSnapshot.md index 004a35db8d4..3f210a026ca 100644 --- a/fixtures/attribute-behavior/AttributeTableSnapshot.md +++ b/fixtures/attribute-behavior/AttributeTableSnapshot.md @@ -2023,6 +2023,56 @@ | `colSpan=(null)`| (initial, ssr error, ssr mismatch)| `` | | `colSpan=(undefined)`| (initial, ssr error, ssr mismatch)| `` | +## `commandFor` (on `, + ); + }); + + const button = container.querySelector('button'); + expect(button.getAttribute('commandfor')).toBe('demo-popover'); + expect(button.getAttribute('command')).toBe('show-popover'); + }); + + it('warns when using commandFor={HTMLElement}', async () => { + const commandTarget = document.createElement('div'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( + , + ); + }); + + assertConsoleErrorDev([ + 'The `commandFor` prop expects the ID of an Element as a string. Received HTMLDivElement {} instead.\n' + + ' in button (at **)', + ]); + + // Dedupe warning + await act(() => { + root.render( + , + ); + }); + }); }); describe('deleteValueForProperty', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index f6447642bce..70e97fffa70 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -730,6 +730,33 @@ describe('ReactDOMEventListener', () => { } }); + it('should not emulate bubbling of command events', async () => { + const container = document.createElement('div'); + const ref = React.createRef(); + const onCommand = jest.fn(); + document.body.appendChild(container); + try { + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ +
, + ); + }); + await act(() => { + ref.current.dispatchEvent( + new Event('command', { + bubbles: false, + }), + ); + }); + expect(onCommand).toHaveBeenCalledTimes(1); + } finally { + document.body.removeChild(container); + } + }); + it('should bubble non-native bubbling cancel/close events', async () => { const container = document.createElement('div'); const ref = React.createRef(); diff --git a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js index 752c8bba961..5c1eb739bba 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js @@ -1434,6 +1434,38 @@ describe('ReactDOMEventListener', () => { }, }); }); + + it('onCommand', async () => { + await testNonBubblingEvent({ + type: 'img', + reactEvent: 'onCommand', + reactEventType: 'command', + nativeEvent: 'command', + dispatch(node) { + const e = new Event('command', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); + + it('onCommand Invoker Commands API', async () => { + await testNonBubblingEvent({ + type: 'div', + reactEvent: 'onCommand', + reactEventType: 'command', + nativeEvent: 'command', + dispatch(node) { + const e = new Event('command', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); }); // The tests for these events are currently very limited 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 f14fcab168b..909b1f251b8 100644 --- a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js +++ b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js @@ -18,6 +18,14 @@ class ToggleEvent extends Event { } } +class CommandEvent extends Event { + constructor(type, eventInit) { + super(type, eventInit); + this.command = eventInit.command; + this.source = eventInit.source; + } +} + describe('SimpleEventPlugin', function () { let React; let ReactDOMClient; @@ -590,6 +598,43 @@ describe('SimpleEventPlugin', function () { }), ); }); + + it('dispatches synthetic command events when the Invoker Commands API is used', async () => { + container = document.createElement('div'); + + const onCommand = jest.fn(); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + <> + + + , + ); + }); + + const target = container.querySelector('#target'); + const source = container.querySelector('button'); + target.dispatchEvent( + new CommandEvent('command', { + bubbles: false, + cancelable: true, + command: '--rotate-left', + source: source, + }), + ); + + expect(onCommand).toHaveBeenCalledTimes(1); + const event = onCommand.mock.calls[0][0]; + expect(event).toEqual( + expect.objectContaining({ + command: '--rotate-left', + source: source, + }), + ); + }); }); it('includes the submitter in submit events', async function () {