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=(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>`)
|
||||
| Test Case | Flags | Result |
|
||||
| --- | --- | --- |
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="popover-target" popover="auto"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
|
|
|||
|
|
@ -1447,6 +1447,22 @@ const attributes = [
|
|||
containerTagName: 'svg',
|
||||
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',
|
||||
tagName: 'video',
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ let didWarnFormActionName = false;
|
|||
let didWarnFormActionTarget = false;
|
||||
let didWarnFormActionMethod = false;
|
||||
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
|
||||
let didWarnPopoverTargetObject = false;
|
||||
let canDiffStyleForHydrationWarning;
|
||||
if (__DEV__) {
|
||||
didWarnForNewBooleanPropsWithEmptyValue = {};
|
||||
|
|
@ -770,6 +771,11 @@ function setProp(
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'popover':
|
||||
listenToNonDelegatedEvent('beforetoggle', domElement);
|
||||
listenToNonDelegatedEvent('toggle', domElement);
|
||||
setValueForAttribute(domElement, 'popover', value);
|
||||
break;
|
||||
case 'xlinkActuate':
|
||||
setValueForNamespacedAttribute(
|
||||
domElement,
|
||||
|
|
@ -861,6 +867,20 @@ function setProp(
|
|||
case 'innerText':
|
||||
case 'textContent':
|
||||
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
|
||||
default: {
|
||||
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) {
|
||||
listenToNonDelegatedEvent('scroll', domElement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type DOMEventName =
|
|||
// 'animationstart' |
|
||||
| 'beforeblur' // Not a real event. This is used by event experiments.
|
||||
| 'beforeinput'
|
||||
| 'beforetoggle'
|
||||
| 'blur'
|
||||
| 'canplay'
|
||||
| 'canplaythrough'
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const topLevelEventsToReactNames: Map<DOMEventName, string | null> =
|
|||
const simpleEventPluginEvents = [
|
||||
'abort',
|
||||
'auxClick',
|
||||
'beforeToggle',
|
||||
'cancel',
|
||||
'canPlay',
|
||||
'canPlayThrough',
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ export const mediaEventTypes: Array<DOMEventName> = [
|
|||
// set them on the actual target element itself. This is primarily
|
||||
// because these events do not consistently bubble in the DOM.
|
||||
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
|
||||
'beforetoggle',
|
||||
'cancel',
|
||||
'close',
|
||||
'invalid',
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
|||
case 'select':
|
||||
case 'selectstart':
|
||||
return DiscreteEventPriority;
|
||||
case 'beforetoggle':
|
||||
case 'drag':
|
||||
case 'dragenter':
|
||||
case 'dragexit':
|
||||
|
|
|
|||
|
|
@ -592,3 +592,11 @@ const WheelEventInterface = {
|
|||
};
|
||||
export const SyntheticWheelEvent: $FlowFixMe =
|
||||
createSyntheticEvent(WheelEventInterface);
|
||||
|
||||
const ToggleEventInterface = {
|
||||
...EventInterface,
|
||||
newState: 0,
|
||||
oldState: 0,
|
||||
};
|
||||
export const SyntheticToggleEvent: $FlowFixMe =
|
||||
createSyntheticEvent(ToggleEventInterface);
|
||||
|
|
|
|||
|
|
@ -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 <details> 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;
|
||||
|
|
|
|||
|
|
@ -344,6 +344,9 @@ const possibleStandardNames = {
|
|||
pointsatx: 'pointsAtX',
|
||||
pointsaty: 'pointsAtY',
|
||||
pointsatz: 'pointsAtZ',
|
||||
popover: 'popover',
|
||||
popovertarget: 'popoverTarget',
|
||||
popovertargetaction: 'popoverTargetAction',
|
||||
prefix: 'prefix',
|
||||
preservealpha: 'preserveAlpha',
|
||||
preserveaspectratio: 'preserveAspectRatio',
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ describe('DOMPropertyOperations', () => {
|
|||
let React;
|
||||
let ReactDOMClient;
|
||||
let act;
|
||||
let assertConsoleErrorDev;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
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,
|
||||
|
|
@ -1317,6 +1318,33 @@ describe('DOMPropertyOperations', () => {
|
|||
});
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
await testEmulatedBubblingEvent({
|
||||
type: 'video',
|
||||
|
|
@ -1969,6 +2003,7 @@ describe('ReactDOMEventListener', () => {
|
|||
type={eventConfig.type}
|
||||
targetRef={targetRef}
|
||||
targetProps={{
|
||||
...eventConfig.targetProps,
|
||||
[eventConfig.reactEvent]: e => {
|
||||
log.push('---- inner');
|
||||
},
|
||||
|
|
@ -2135,11 +2170,10 @@ describe('ReactDOMEventListener', () => {
|
|||
<Fixture
|
||||
type={eventConfig.type}
|
||||
targetRef={targetRef}
|
||||
targetProps={
|
||||
{
|
||||
// No listener on the target itself.
|
||||
}
|
||||
}
|
||||
targetProps={{
|
||||
...eventConfig.targetProps,
|
||||
// No listener on the target itself.
|
||||
}}
|
||||
parentProps={{
|
||||
[eventConfig.reactEvent]: e => {
|
||||
log.push('--- inner parent');
|
||||
|
|
@ -2368,6 +2402,7 @@ describe('ReactDOMEventListener', () => {
|
|||
type={eventConfig.type}
|
||||
targetRef={targetRef}
|
||||
targetProps={{
|
||||
...eventConfig.targetProps,
|
||||
[eventConfig.reactEvent]: e => {
|
||||
e.stopPropagation(); // <---------
|
||||
log.push('---- inner');
|
||||
|
|
@ -2705,6 +2740,7 @@ describe('ReactDOMEventListener', () => {
|
|||
}
|
||||
}}
|
||||
targetProps={{
|
||||
...eventConfig.targetProps,
|
||||
[eventConfig.reactEvent]: e => {
|
||||
log.push('---- inner');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ describe('ReactTestUtils', () => {
|
|||
"animationStart",
|
||||
"auxClick",
|
||||
"beforeInput",
|
||||
"beforeToggle",
|
||||
"blur",
|
||||
"canPlay",
|
||||
"canPlayThrough",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<>
|
||||
<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',
|
||||
'select',
|
||||
'beforeInput',
|
||||
'beforeToggle',
|
||||
'compositionEnd',
|
||||
'compositionStart',
|
||||
'compositionUpdate',
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user