mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
React DOM: Add support for Popover API (#27981)
This commit is contained in:
parent
d3ce0d3ea9
commit
6f90365128
|
|
@ -8448,6 +8448,81 @@
|
||||||
| `pointsAtZ=(null)`| (initial)| `<number: 0>` |
|
| `pointsAtZ=(null)`| (initial)| `<number: 0>` |
|
||||||
| `pointsAtZ=(undefined)`| (initial)| `<number: 0>` |
|
| `pointsAtZ=(undefined)`| (initial)| `<number: 0>` |
|
||||||
|
|
||||||
|
## `popover` (on `<div>` inside `<div>`)
|
||||||
|
| Test Case | Flags | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `popover=(string)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(empty string)`| (changed)| `"auto"` |
|
||||||
|
| `popover=(array with string)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(empty array)`| (changed)| `"auto"` |
|
||||||
|
| `popover=(object)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(numeric string)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(-1)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(0)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(integer)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(NaN)`| (changed, warning)| `"manual"` |
|
||||||
|
| `popover=(float)`| (changed)| `"manual"` |
|
||||||
|
| `popover=(true)`| (initial, warning)| `<null>` |
|
||||||
|
| `popover=(false)`| (initial, warning)| `<null>` |
|
||||||
|
| `popover=(string 'true')`| (changed)| `"manual"` |
|
||||||
|
| `popover=(string 'false')`| (changed)| `"manual"` |
|
||||||
|
| `popover=(string 'on')`| (changed)| `"manual"` |
|
||||||
|
| `popover=(string 'off')`| (changed)| `"manual"` |
|
||||||
|
| `popover=(symbol)`| (initial, warning)| `<null>` |
|
||||||
|
| `popover=(function)`| (initial, warning)| `<null>` |
|
||||||
|
| `popover=(null)`| (initial)| `<null>` |
|
||||||
|
| `popover=(undefined)`| (initial)| `<null>` |
|
||||||
|
|
||||||
|
## `popoverTarget` (on `<button>` inside `<div>`)
|
||||||
|
| Test Case | Flags | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `popoverTarget=(string)`| (changed)| `<HTMLDivElement>` |
|
||||||
|
| `popoverTarget=(empty string)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(array with string)`| (changed, warning, ssr warning)| `<HTMLDivElement>` |
|
||||||
|
| `popoverTarget=(empty array)`| (initial, warning, ssr warning)| `<null>` |
|
||||||
|
| `popoverTarget=(object)`| (initial, warning, ssr warning)| `<null>` |
|
||||||
|
| `popoverTarget=(numeric string)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(-1)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(0)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(integer)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(NaN)`| (initial, warning)| `<null>` |
|
||||||
|
| `popoverTarget=(float)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(true)`| (initial, warning)| `<null>` |
|
||||||
|
| `popoverTarget=(false)`| (initial, warning)| `<null>` |
|
||||||
|
| `popoverTarget=(string 'true')`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(string 'false')`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(string 'on')`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(string 'off')`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(symbol)`| (initial, warning)| `<null>` |
|
||||||
|
| `popoverTarget=(function)`| (initial, warning)| `<null>` |
|
||||||
|
| `popoverTarget=(null)`| (initial)| `<null>` |
|
||||||
|
| `popoverTarget=(undefined)`| (initial)| `<null>` |
|
||||||
|
|
||||||
|
## `popoverTargetAction` (on `<button>` inside `<div>`)
|
||||||
|
| Test Case | Flags | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `popoverTargetAction=(string)`| (changed)| `"show"` |
|
||||||
|
| `popoverTargetAction=(empty string)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(array with string)`| (changed)| `"show"` |
|
||||||
|
| `popoverTargetAction=(empty array)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(object)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(numeric string)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(-1)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(0)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(integer)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(NaN)`| (initial, warning)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(float)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(true)`| (initial, warning)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(false)`| (initial, warning)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(string 'true')`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(string 'false')`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(string 'on')`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(string 'off')`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(symbol)`| (initial, warning)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(function)`| (initial, warning)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(null)`| (initial)| `"toggle"` |
|
||||||
|
| `popoverTargetAction=(undefined)`| (initial)| `"toggle"` |
|
||||||
|
|
||||||
## `poster` (on `<video>` inside `<div>`)
|
## `poster` (on `<video>` inside `<div>`)
|
||||||
| Test Case | Flags | Result |
|
| Test Case | Flags | Result |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
You need to enable JavaScript to run this app.
|
You need to enable JavaScript to run this app.
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<div id="popover-target" popover="auto"></div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
|
||||||
|
|
@ -1447,6 +1447,22 @@ const attributes = [
|
||||||
containerTagName: 'svg',
|
containerTagName: 'svg',
|
||||||
tagName: 'feSpotLight',
|
tagName: 'feSpotLight',
|
||||||
},
|
},
|
||||||
|
{name: 'popover', overrideStringValue: 'manual'},
|
||||||
|
{
|
||||||
|
name: 'popoverTarget',
|
||||||
|
read: element => {
|
||||||
|
document.body.appendChild(element);
|
||||||
|
try {
|
||||||
|
// trigger and target need to be connected for `popoverTargetElement` to read the actual value.
|
||||||
|
return element.popoverTargetElement;
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overrideStringValue: 'popover-target',
|
||||||
|
tagName: 'button',
|
||||||
|
},
|
||||||
|
{name: 'popoverTargetAction', overrideStringValue: 'show', tagName: 'button'},
|
||||||
{
|
{
|
||||||
name: 'poster',
|
name: 'poster',
|
||||||
tagName: 'video',
|
tagName: 'video',
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ let didWarnFormActionName = false;
|
||||||
let didWarnFormActionTarget = false;
|
let didWarnFormActionTarget = false;
|
||||||
let didWarnFormActionMethod = false;
|
let didWarnFormActionMethod = false;
|
||||||
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
|
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
|
||||||
|
let didWarnPopoverTargetObject = false;
|
||||||
let canDiffStyleForHydrationWarning;
|
let canDiffStyleForHydrationWarning;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
didWarnForNewBooleanPropsWithEmptyValue = {};
|
didWarnForNewBooleanPropsWithEmptyValue = {};
|
||||||
|
|
@ -770,6 +771,11 @@ function setProp(
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'popover':
|
||||||
|
listenToNonDelegatedEvent('beforetoggle', domElement);
|
||||||
|
listenToNonDelegatedEvent('toggle', domElement);
|
||||||
|
setValueForAttribute(domElement, 'popover', value);
|
||||||
|
break;
|
||||||
case 'xlinkActuate':
|
case 'xlinkActuate':
|
||||||
setValueForNamespacedAttribute(
|
setValueForNamespacedAttribute(
|
||||||
domElement,
|
domElement,
|
||||||
|
|
@ -861,6 +867,20 @@ function setProp(
|
||||||
case 'innerText':
|
case 'innerText':
|
||||||
case 'textContent':
|
case 'textContent':
|
||||||
break;
|
break;
|
||||||
|
case 'popoverTarget':
|
||||||
|
if (__DEV__) {
|
||||||
|
if (
|
||||||
|
!didWarnPopoverTargetObject &&
|
||||||
|
value != null &&
|
||||||
|
typeof value === 'object'
|
||||||
|
) {
|
||||||
|
didWarnPopoverTargetObject = true;
|
||||||
|
console.error(
|
||||||
|
'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Fall through
|
// Fall through
|
||||||
default: {
|
default: {
|
||||||
if (
|
if (
|
||||||
|
|
@ -2953,6 +2973,13 @@ export function hydrateProperties(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.popover != null) {
|
||||||
|
// We listen to this event in case to ensure emulated bubble
|
||||||
|
// listeners still fire for the toggle event.
|
||||||
|
listenToNonDelegatedEvent('beforetoggle', domElement);
|
||||||
|
listenToNonDelegatedEvent('toggle', domElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (props.onScroll != null) {
|
if (props.onScroll != null) {
|
||||||
listenToNonDelegatedEvent('scroll', domElement);
|
listenToNonDelegatedEvent('scroll', domElement);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export type DOMEventName =
|
||||||
// 'animationstart' |
|
// 'animationstart' |
|
||||||
| 'beforeblur' // Not a real event. This is used by event experiments.
|
| 'beforeblur' // Not a real event. This is used by event experiments.
|
||||||
| 'beforeinput'
|
| 'beforeinput'
|
||||||
|
| 'beforetoggle'
|
||||||
| 'blur'
|
| 'blur'
|
||||||
| 'canplay'
|
| 'canplay'
|
||||||
| 'canplaythrough'
|
| 'canplaythrough'
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export const topLevelEventsToReactNames: Map<DOMEventName, string | null> =
|
||||||
const simpleEventPluginEvents = [
|
const simpleEventPluginEvents = [
|
||||||
'abort',
|
'abort',
|
||||||
'auxClick',
|
'auxClick',
|
||||||
|
'beforeToggle',
|
||||||
'cancel',
|
'cancel',
|
||||||
'canPlay',
|
'canPlay',
|
||||||
'canPlayThrough',
|
'canPlayThrough',
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,7 @@ export const mediaEventTypes: Array<DOMEventName> = [
|
||||||
// set them on the actual target element itself. This is primarily
|
// set them on the actual target element itself. This is primarily
|
||||||
// because these events do not consistently bubble in the DOM.
|
// because these events do not consistently bubble in the DOM.
|
||||||
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
|
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
|
||||||
|
'beforetoggle',
|
||||||
'cancel',
|
'cancel',
|
||||||
'close',
|
'close',
|
||||||
'invalid',
|
'invalid',
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||||
case 'select':
|
case 'select':
|
||||||
case 'selectstart':
|
case 'selectstart':
|
||||||
return DiscreteEventPriority;
|
return DiscreteEventPriority;
|
||||||
|
case 'beforetoggle':
|
||||||
case 'drag':
|
case 'drag':
|
||||||
case 'dragenter':
|
case 'dragenter':
|
||||||
case 'dragexit':
|
case 'dragexit':
|
||||||
|
|
|
||||||
|
|
@ -592,3 +592,11 @@ const WheelEventInterface = {
|
||||||
};
|
};
|
||||||
export const SyntheticWheelEvent: $FlowFixMe =
|
export const SyntheticWheelEvent: $FlowFixMe =
|
||||||
createSyntheticEvent(WheelEventInterface);
|
createSyntheticEvent(WheelEventInterface);
|
||||||
|
|
||||||
|
const ToggleEventInterface = {
|
||||||
|
...EventInterface,
|
||||||
|
newState: 0,
|
||||||
|
oldState: 0,
|
||||||
|
};
|
||||||
|
export const SyntheticToggleEvent: $FlowFixMe =
|
||||||
|
createSyntheticEvent(ToggleEventInterface);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
SyntheticWheelEvent,
|
SyntheticWheelEvent,
|
||||||
SyntheticClipboardEvent,
|
SyntheticClipboardEvent,
|
||||||
SyntheticPointerEvent,
|
SyntheticPointerEvent,
|
||||||
|
SyntheticToggleEvent,
|
||||||
} from '../../events/SyntheticEvent';
|
} from '../../events/SyntheticEvent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -161,6 +162,11 @@ function extractEvents(
|
||||||
case 'pointerup':
|
case 'pointerup':
|
||||||
SyntheticEventCtor = SyntheticPointerEvent;
|
SyntheticEventCtor = SyntheticPointerEvent;
|
||||||
break;
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
case 'beforetoggle':
|
||||||
|
// MDN claims <details> should not receive ToggleEvent contradicting the spec: https://html.spec.whatwg.org/multipage/indices.html#event-toggle
|
||||||
|
SyntheticEventCtor = SyntheticToggleEvent;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown event. This is used by createEventHandle.
|
// Unknown event. This is used by createEventHandle.
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,9 @@ const possibleStandardNames = {
|
||||||
pointsatx: 'pointsAtX',
|
pointsatx: 'pointsAtX',
|
||||||
pointsaty: 'pointsAtY',
|
pointsaty: 'pointsAtY',
|
||||||
pointsatz: 'pointsAtZ',
|
pointsatz: 'pointsAtZ',
|
||||||
|
popover: 'popover',
|
||||||
|
popovertarget: 'popoverTarget',
|
||||||
|
popovertargetaction: 'popoverTargetAction',
|
||||||
prefix: 'prefix',
|
prefix: 'prefix',
|
||||||
preservealpha: 'preserveAlpha',
|
preservealpha: 'preserveAlpha',
|
||||||
preserveaspectratio: 'preserveAspectRatio',
|
preserveaspectratio: 'preserveAspectRatio',
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ describe('DOMPropertyOperations', () => {
|
||||||
let React;
|
let React;
|
||||||
let ReactDOMClient;
|
let ReactDOMClient;
|
||||||
let act;
|
let act;
|
||||||
|
let assertConsoleErrorDev;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
React = require('react');
|
React = require('react');
|
||||||
ReactDOMClient = require('react-dom/client');
|
ReactDOMClient = require('react-dom/client');
|
||||||
({act} = require('internal-test-utils'));
|
({act, assertConsoleErrorDev} = require('internal-test-utils'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets a value in a way that React doesn't see,
|
// Sets a value in a way that React doesn't see,
|
||||||
|
|
@ -1317,6 +1318,33 @@ describe('DOMPropertyOperations', () => {
|
||||||
});
|
});
|
||||||
expect(customElement.foo).toBe(undefined);
|
expect(customElement.foo).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('warns when using popoverTarget={HTMLElement}', async () => {
|
||||||
|
const popoverTarget = document.createElement('div');
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const root = ReactDOMClient.createRoot(container);
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
root.render(
|
||||||
|
<button key="one" popoverTarget={popoverTarget}>
|
||||||
|
Toggle popover
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertConsoleErrorDev([
|
||||||
|
'The `popoverTarget` prop expects the ID of an Element as a string. Received [object HTMLDivElement] instead.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dedupe warning
|
||||||
|
await act(() => {
|
||||||
|
root.render(
|
||||||
|
<button key="two" popoverTarget={popoverTarget}>
|
||||||
|
Toggle popover
|
||||||
|
</button>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteValueForProperty', () => {
|
describe('deleteValueForProperty', () => {
|
||||||
|
|
|
||||||
|
|
@ -1268,6 +1268,40 @@ describe('ReactDOMEventListener', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('onBeforeToggle Popover API', async () => {
|
||||||
|
await testEmulatedBubblingEvent({
|
||||||
|
type: 'div',
|
||||||
|
targetProps: {popover: 'any'},
|
||||||
|
reactEvent: 'onBeforeToggle',
|
||||||
|
reactEventType: 'beforetoggle',
|
||||||
|
nativeEvent: 'beforetoggle',
|
||||||
|
dispatch(node) {
|
||||||
|
const e = new Event('beforetoggle', {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
node.dispatchEvent(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onToggle Popover API', async () => {
|
||||||
|
await testEmulatedBubblingEvent({
|
||||||
|
type: 'div',
|
||||||
|
targetProps: {popover: 'any'},
|
||||||
|
reactEvent: 'onToggle',
|
||||||
|
reactEventType: 'toggle',
|
||||||
|
nativeEvent: 'toggle',
|
||||||
|
dispatch(node) {
|
||||||
|
const e = new Event('toggle', {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
node.dispatchEvent(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('onVolumeChange', async () => {
|
it('onVolumeChange', async () => {
|
||||||
await testEmulatedBubblingEvent({
|
await testEmulatedBubblingEvent({
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
|
@ -1969,6 +2003,7 @@ describe('ReactDOMEventListener', () => {
|
||||||
type={eventConfig.type}
|
type={eventConfig.type}
|
||||||
targetRef={targetRef}
|
targetRef={targetRef}
|
||||||
targetProps={{
|
targetProps={{
|
||||||
|
...eventConfig.targetProps,
|
||||||
[eventConfig.reactEvent]: e => {
|
[eventConfig.reactEvent]: e => {
|
||||||
log.push('---- inner');
|
log.push('---- inner');
|
||||||
},
|
},
|
||||||
|
|
@ -2135,11 +2170,10 @@ describe('ReactDOMEventListener', () => {
|
||||||
<Fixture
|
<Fixture
|
||||||
type={eventConfig.type}
|
type={eventConfig.type}
|
||||||
targetRef={targetRef}
|
targetRef={targetRef}
|
||||||
targetProps={
|
targetProps={{
|
||||||
{
|
...eventConfig.targetProps,
|
||||||
// No listener on the target itself.
|
// No listener on the target itself.
|
||||||
}
|
}}
|
||||||
}
|
|
||||||
parentProps={{
|
parentProps={{
|
||||||
[eventConfig.reactEvent]: e => {
|
[eventConfig.reactEvent]: e => {
|
||||||
log.push('--- inner parent');
|
log.push('--- inner parent');
|
||||||
|
|
@ -2368,6 +2402,7 @@ describe('ReactDOMEventListener', () => {
|
||||||
type={eventConfig.type}
|
type={eventConfig.type}
|
||||||
targetRef={targetRef}
|
targetRef={targetRef}
|
||||||
targetProps={{
|
targetProps={{
|
||||||
|
...eventConfig.targetProps,
|
||||||
[eventConfig.reactEvent]: e => {
|
[eventConfig.reactEvent]: e => {
|
||||||
e.stopPropagation(); // <---------
|
e.stopPropagation(); // <---------
|
||||||
log.push('---- inner');
|
log.push('---- inner');
|
||||||
|
|
@ -2705,6 +2740,7 @@ describe('ReactDOMEventListener', () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
targetProps={{
|
targetProps={{
|
||||||
|
...eventConfig.targetProps,
|
||||||
[eventConfig.reactEvent]: e => {
|
[eventConfig.reactEvent]: e => {
|
||||||
log.push('---- inner');
|
log.push('---- inner');
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ describe('ReactTestUtils', () => {
|
||||||
"animationStart",
|
"animationStart",
|
||||||
"auxClick",
|
"auxClick",
|
||||||
"beforeInput",
|
"beforeInput",
|
||||||
|
"beforeToggle",
|
||||||
"blur",
|
"blur",
|
||||||
"canPlay",
|
"canPlay",
|
||||||
"canPlayThrough",
|
"canPlayThrough",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,15 @@
|
||||||
|
|
||||||
'use strict';
|
'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 () {
|
describe('SimpleEventPlugin', function () {
|
||||||
let React;
|
let React;
|
||||||
let ReactDOMClient;
|
let ReactDOMClient;
|
||||||
|
|
@ -469,5 +478,116 @@ describe('SimpleEventPlugin', function () {
|
||||||
'wheel',
|
'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(
|
||||||
|
<>
|
||||||
|
<button popoverTarget="popover">Toggle popover</button>
|
||||||
|
<div id="popover" popover="" onToggle={onToggle}>
|
||||||
|
popover content
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 <details> is used', async () => {
|
||||||
|
// This test just replays browser behavior.
|
||||||
|
// The real test would be if browsers dispatch ToggleEvent on <details>.
|
||||||
|
// This case only exists because MDN claims <details> doesn't receive ToggleEvent.
|
||||||
|
// However, Chrome dispatches ToggleEvent on <details> 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(
|
||||||
|
<details id="details" onToggle={onToggle}>
|
||||||
|
<summary>Summary</summary>
|
||||||
|
Details
|
||||||
|
</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',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -850,6 +850,7 @@ const simulatedEventTypes = [
|
||||||
'change',
|
'change',
|
||||||
'select',
|
'select',
|
||||||
'beforeInput',
|
'beforeInput',
|
||||||
|
'beforeToggle',
|
||||||
'compositionEnd',
|
'compositionEnd',
|
||||||
'compositionStart',
|
'compositionStart',
|
||||||
'compositionUpdate',
|
'compositionUpdate',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user