Skip to content

Commit 6f90365

Browse files
authored
React DOM: Add support for Popover API (#27981)
1 parent d3ce0d3 commit 6f90365

File tree

16 files changed

+332
-6
lines changed

16 files changed

+332
-6
lines changed

fixtures/attribute-behavior/AttributeTableSnapshot.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8448,6 +8448,81 @@
84488448
| `pointsAtZ=(null)`| (initial)| `<number: 0>` |
84498449
| `pointsAtZ=(undefined)`| (initial)| `<number: 0>` |
84508450

8451+
## `popover` (on `<div>` inside `<div>`)
8452+
| Test Case | Flags | Result |
8453+
| --- | --- | --- |
8454+
| `popover=(string)`| (changed)| `"manual"` |
8455+
| `popover=(empty string)`| (changed)| `"auto"` |
8456+
| `popover=(array with string)`| (changed)| `"manual"` |
8457+
| `popover=(empty array)`| (changed)| `"auto"` |
8458+
| `popover=(object)`| (changed)| `"manual"` |
8459+
| `popover=(numeric string)`| (changed)| `"manual"` |
8460+
| `popover=(-1)`| (changed)| `"manual"` |
8461+
| `popover=(0)`| (changed)| `"manual"` |
8462+
| `popover=(integer)`| (changed)| `"manual"` |
8463+
| `popover=(NaN)`| (changed, warning)| `"manual"` |
8464+
| `popover=(float)`| (changed)| `"manual"` |
8465+
| `popover=(true)`| (initial, warning)| `<null>` |
8466+
| `popover=(false)`| (initial, warning)| `<null>` |
8467+
| `popover=(string 'true')`| (changed)| `"manual"` |
8468+
| `popover=(string 'false')`| (changed)| `"manual"` |
8469+
| `popover=(string 'on')`| (changed)| `"manual"` |
8470+
| `popover=(string 'off')`| (changed)| `"manual"` |
8471+
| `popover=(symbol)`| (initial, warning)| `<null>` |
8472+
| `popover=(function)`| (initial, warning)| `<null>` |
8473+
| `popover=(null)`| (initial)| `<null>` |
8474+
| `popover=(undefined)`| (initial)| `<null>` |
8475+
8476+
## `popoverTarget` (on `<button>` inside `<div>`)
8477+
| Test Case | Flags | Result |
8478+
| --- | --- | --- |
8479+
| `popoverTarget=(string)`| (changed)| `<HTMLDivElement>` |
8480+
| `popoverTarget=(empty string)`| (initial)| `<null>` |
8481+
| `popoverTarget=(array with string)`| (changed, warning, ssr warning)| `<HTMLDivElement>` |
8482+
| `popoverTarget=(empty array)`| (initial, warning, ssr warning)| `<null>` |
8483+
| `popoverTarget=(object)`| (initial, warning, ssr warning)| `<null>` |
8484+
| `popoverTarget=(numeric string)`| (initial)| `<null>` |
8485+
| `popoverTarget=(-1)`| (initial)| `<null>` |
8486+
| `popoverTarget=(0)`| (initial)| `<null>` |
8487+
| `popoverTarget=(integer)`| (initial)| `<null>` |
8488+
| `popoverTarget=(NaN)`| (initial, warning)| `<null>` |
8489+
| `popoverTarget=(float)`| (initial)| `<null>` |
8490+
| `popoverTarget=(true)`| (initial, warning)| `<null>` |
8491+
| `popoverTarget=(false)`| (initial, warning)| `<null>` |
8492+
| `popoverTarget=(string 'true')`| (initial)| `<null>` |
8493+
| `popoverTarget=(string 'false')`| (initial)| `<null>` |
8494+
| `popoverTarget=(string 'on')`| (initial)| `<null>` |
8495+
| `popoverTarget=(string 'off')`| (initial)| `<null>` |
8496+
| `popoverTarget=(symbol)`| (initial, warning)| `<null>` |
8497+
| `popoverTarget=(function)`| (initial, warning)| `<null>` |
8498+
| `popoverTarget=(null)`| (initial)| `<null>` |
8499+
| `popoverTarget=(undefined)`| (initial)| `<null>` |
8500+
8501+
## `popoverTargetAction` (on `<button>` inside `<div>`)
8502+
| Test Case | Flags | Result |
8503+
| --- | --- | --- |
8504+
| `popoverTargetAction=(string)`| (changed)| `"show"` |
8505+
| `popoverTargetAction=(empty string)`| (initial)| `"toggle"` |
8506+
| `popoverTargetAction=(array with string)`| (changed)| `"show"` |
8507+
| `popoverTargetAction=(empty array)`| (initial)| `"toggle"` |
8508+
| `popoverTargetAction=(object)`| (initial)| `"toggle"` |
8509+
| `popoverTargetAction=(numeric string)`| (initial)| `"toggle"` |
8510+
| `popoverTargetAction=(-1)`| (initial)| `"toggle"` |
8511+
| `popoverTargetAction=(0)`| (initial)| `"toggle"` |
8512+
| `popoverTargetAction=(integer)`| (initial)| `"toggle"` |
8513+
| `popoverTargetAction=(NaN)`| (initial, warning)| `"toggle"` |
8514+
| `popoverTargetAction=(float)`| (initial)| `"toggle"` |
8515+
| `popoverTargetAction=(true)`| (initial, warning)| `"toggle"` |
8516+
| `popoverTargetAction=(false)`| (initial, warning)| `"toggle"` |
8517+
| `popoverTargetAction=(string 'true')`| (initial)| `"toggle"` |
8518+
| `popoverTargetAction=(string 'false')`| (initial)| `"toggle"` |
8519+
| `popoverTargetAction=(string 'on')`| (initial)| `"toggle"` |
8520+
| `popoverTargetAction=(string 'off')`| (initial)| `"toggle"` |
8521+
| `popoverTargetAction=(symbol)`| (initial, warning)| `"toggle"` |
8522+
| `popoverTargetAction=(function)`| (initial, warning)| `"toggle"` |
8523+
| `popoverTargetAction=(null)`| (initial)| `"toggle"` |
8524+
| `popoverTargetAction=(undefined)`| (initial)| `"toggle"` |
8525+
84518526
## `poster` (on `<video>` inside `<div>`)
84528527
| Test Case | Flags | Result |
84538528
| --- | --- | --- |

fixtures/attribute-behavior/public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
You need to enable JavaScript to run this app.
2727
</noscript>
2828
<div id="root"></div>
29+
<div id="popover-target" popover="auto"></div>
2930
<!--
3031
This HTML file is a template.
3132
If you open it directly in the browser, you will see an empty page.

fixtures/attribute-behavior/src/attributes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,22 @@ const attributes = [
14471447
containerTagName: 'svg',
14481448
tagName: 'feSpotLight',
14491449
},
1450+
{name: 'popover', overrideStringValue: 'manual'},
1451+
{
1452+
name: 'popoverTarget',
1453+
read: element => {
1454+
document.body.appendChild(element);
1455+
try {
1456+
// trigger and target need to be connected for `popoverTargetElement` to read the actual value.
1457+
return element.popoverTargetElement;
1458+
} finally {
1459+
document.body.removeChild(element);
1460+
}
1461+
},
1462+
overrideStringValue: 'popover-target',
1463+
tagName: 'button',
1464+
},
1465+
{name: 'popoverTargetAction', overrideStringValue: 'show', tagName: 'button'},
14501466
{
14511467
name: 'poster',
14521468
tagName: 'video',

packages/react-dom-bindings/src/client/ReactDOMComponent.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ let didWarnFormActionName = false;
8282
let didWarnFormActionTarget = false;
8383
let didWarnFormActionMethod = false;
8484
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
85+
let didWarnPopoverTargetObject = false;
8586
let canDiffStyleForHydrationWarning;
8687
if (__DEV__) {
8788
didWarnForNewBooleanPropsWithEmptyValue = {};
@@ -770,6 +771,11 @@ function setProp(
770771
}
771772
break;
772773
}
774+
case 'popover':
775+
listenToNonDelegatedEvent('beforetoggle', domElement);
776+
listenToNonDelegatedEvent('toggle', domElement);
777+
setValueForAttribute(domElement, 'popover', value);
778+
break;
773779
case 'xlinkActuate':
774780
setValueForNamespacedAttribute(
775781
domElement,
@@ -861,6 +867,20 @@ function setProp(
861867
case 'innerText':
862868
case 'textContent':
863869
break;
870+
case 'popoverTarget':
871+
if (__DEV__) {
872+
if (
873+
!didWarnPopoverTargetObject &&
874+
value != null &&
875+
typeof value === 'object'
876+
) {
877+
didWarnPopoverTargetObject = true;
878+
console.error(
879+
'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
880+
value,
881+
);
882+
}
883+
}
864884
// Fall through
865885
default: {
866886
if (
@@ -2953,6 +2973,13 @@ export function hydrateProperties(
29532973
}
29542974
}
29552975

2976+
if (props.popover != null) {
2977+
// We listen to this event in case to ensure emulated bubble
2978+
// listeners still fire for the toggle event.
2979+
listenToNonDelegatedEvent('beforetoggle', domElement);
2980+
listenToNonDelegatedEvent('toggle', domElement);
2981+
}
2982+
29562983
if (props.onScroll != null) {
29572984
listenToNonDelegatedEvent('scroll', domElement);
29582985
}

packages/react-dom-bindings/src/events/DOMEventNames.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type DOMEventName =
1818
// 'animationstart' |
1919
| 'beforeblur' // Not a real event. This is used by event experiments.
2020
| 'beforeinput'
21+
| 'beforetoggle'
2122
| 'blur'
2223
| 'canplay'
2324
| 'canplaythrough'

packages/react-dom-bindings/src/events/DOMEventProperties.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const topLevelEventsToReactNames: Map<DOMEventName, string | null> =
3737
const simpleEventPluginEvents = [
3838
'abort',
3939
'auxClick',
40+
'beforeToggle',
4041
'cancel',
4142
'canPlay',
4243
'canPlayThrough',

packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export const mediaEventTypes: Array<DOMEventName> = [
214214
// set them on the actual target element itself. This is primarily
215215
// because these events do not consistently bubble in the DOM.
216216
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
217+
'beforetoggle',
217218
'cancel',
218219
'close',
219220
'invalid',

packages/react-dom-bindings/src/events/ReactDOMEventListener.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
345345
case 'select':
346346
case 'selectstart':
347347
return DiscreteEventPriority;
348+
case 'beforetoggle':
348349
case 'drag':
349350
case 'dragenter':
350351
case 'dragexit':

packages/react-dom-bindings/src/events/SyntheticEvent.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,11 @@ const WheelEventInterface = {
592592
};
593593
export const SyntheticWheelEvent: $FlowFixMe =
594594
createSyntheticEvent(WheelEventInterface);
595+
596+
const ToggleEventInterface = {
597+
...EventInterface,
598+
newState: 0,
599+
oldState: 0,
600+
};
601+
export const SyntheticToggleEvent: $FlowFixMe =
602+
createSyntheticEvent(ToggleEventInterface);

packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SyntheticWheelEvent,
2828
SyntheticClipboardEvent,
2929
SyntheticPointerEvent,
30+
SyntheticToggleEvent,
3031
} from '../../events/SyntheticEvent';
3132

3233
import {
@@ -161,6 +162,11 @@ function extractEvents(
161162
case 'pointerup':
162163
SyntheticEventCtor = SyntheticPointerEvent;
163164
break;
165+
case 'toggle':
166+
case 'beforetoggle':
167+
// MDN claims <details> should not receive ToggleEvent contradicting the spec: https://html.spec.whatwg.org/multipage/indices.html#event-toggle
168+
SyntheticEventCtor = SyntheticToggleEvent;
169+
break;
164170
default:
165171
// Unknown event. This is used by createEventHandle.
166172
break;

0 commit comments

Comments
 (0)