@primer/react 38.36.0-rc.62d935135 → 38.36.0-rc.a1228eda5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/dist/Portal/Portal.js +91 -29
- package/dist/hooks/useMenuInitialFocus.js +72 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
### Patch Changes
|
|
11
11
|
|
|
12
|
+
- [#8276](https://github.com/primer/react/pull/8276) [`5069f8a`](https://github.com/primer/react/commit/5069f8a7cb95544ce14122e97d67c8a1279f8a08) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Portal: Render nothing while server rendering and during hydration instead of accessing the DOM during render, fixing server rendering support. Client-only renders are unchanged and still mount portaled content in the same commit.
|
|
13
|
+
|
|
12
14
|
- [#8260](https://github.com/primer/react/pull/8260) [`24d0831`](https://github.com/primer/react/commit/24d0831d34b13e3b4bac8afcfa2ccd5795cd9a9d) Thanks [@liuliu-dev](https://github.com/liuliu-dev)! - UnderlineNav: Keep item icons visible by default at the medium breakpoint and above
|
|
13
15
|
|
|
14
16
|
## 38.35.1
|
package/dist/Portal/Portal.js
CHANGED
|
@@ -1,43 +1,105 @@
|
|
|
1
1
|
import useIsomorphicLayoutEffect from "../utils/useIsomorphicLayoutEffect.js";
|
|
2
2
|
import { PortalContext } from "./PortalContext.js";
|
|
3
3
|
import { DEFAULT_PORTAL_CONTAINER_NAME, ensureDefaultPortal, getPortalRoot } from "./portalRoot.js";
|
|
4
|
-
import
|
|
4
|
+
import { c } from "react-compiler-runtime";
|
|
5
|
+
import React, { useContext, useSyncExternalStore } from "react";
|
|
5
6
|
import { createPortal } from "react-dom";
|
|
6
7
|
//#region src/Portal/Portal.tsx
|
|
8
|
+
const subscribe = () => () => {};
|
|
9
|
+
const getSnapshot = () => true;
|
|
10
|
+
const getServerSnapshot = () => false;
|
|
11
|
+
/**
|
|
12
|
+
* Returns `true` only when React is rendering on the client, and not as part of
|
|
13
|
+
* hydrating server-rendered markup.
|
|
14
|
+
*
|
|
15
|
+
* `Portal` needs this because React's server renderer throws when it encounters a
|
|
16
|
+
* portal, and portaled content is never part of the server-rendered markup.
|
|
17
|
+
*
|
|
18
|
+
* `useSyncExternalStore` is used rather than a `useEffect`/`useState` "is mounted"
|
|
19
|
+
* flag because it only defers the portal for the renders that need it: it returns
|
|
20
|
+
* `false` while server rendering and for the hydration pass (keeping hydration
|
|
21
|
+
* consistent with the server markup), but `true` from the very first render of a
|
|
22
|
+
* client-only render. That way client-rendered portals — the overwhelmingly common
|
|
23
|
+
* case for overlays, dialogs and menus — still mount their children in the same
|
|
24
|
+
* commit as the `Portal` itself, so refs into portaled content are populated by the
|
|
25
|
+
* time the parent's layout effects run.
|
|
26
|
+
*/
|
|
27
|
+
function useIsClientRender() {
|
|
28
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
29
|
+
}
|
|
7
30
|
/**
|
|
8
31
|
* Creates a React Portal, placing all children in a separate physical DOM root node.
|
|
9
32
|
* @see https://reactjs.org/docs/portals.html
|
|
10
33
|
*/
|
|
11
|
-
const Portal = (
|
|
34
|
+
const Portal = (t0) => {
|
|
35
|
+
const $ = c(12);
|
|
36
|
+
const { children, onMount, containerName: _containerName } = t0;
|
|
12
37
|
const { portalContainerName } = useContext(PortalContext);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
elementRef.current = div;
|
|
20
|
-
}
|
|
21
|
-
const element = elementRef.current;
|
|
22
|
-
useIsomorphicLayoutEffect(() => {
|
|
23
|
-
let containerName = _containerName !== null && _containerName !== void 0 ? _containerName : portalContainerName;
|
|
24
|
-
if (containerName === void 0) {
|
|
25
|
-
containerName = DEFAULT_PORTAL_CONTAINER_NAME;
|
|
26
|
-
ensureDefaultPortal();
|
|
27
|
-
}
|
|
28
|
-
const parentElement = getPortalRoot(containerName);
|
|
29
|
-
if (!parentElement) throw new Error(`Portal container '${containerName}' is not yet registered. Container must be registered with registerPortalRoot before use.`);
|
|
30
|
-
parentElement.appendChild(element);
|
|
31
|
-
onMount === null || onMount === void 0 || onMount();
|
|
32
|
-
return () => {
|
|
33
|
-
parentElement.removeChild(element);
|
|
38
|
+
const isClientRender = useIsClientRender();
|
|
39
|
+
const onMountRef = React.useRef(onMount);
|
|
40
|
+
let t1;
|
|
41
|
+
if ($[0] !== onMount) {
|
|
42
|
+
t1 = () => {
|
|
43
|
+
onMountRef.current = onMount;
|
|
34
44
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
]);
|
|
40
|
-
|
|
45
|
+
$[0] = onMount;
|
|
46
|
+
$[1] = t1;
|
|
47
|
+
} else t1 = $[1];
|
|
48
|
+
useIsomorphicLayoutEffect(t1);
|
|
49
|
+
const [element] = React.useState(_temp);
|
|
50
|
+
let t2;
|
|
51
|
+
let t3;
|
|
52
|
+
if ($[2] !== _containerName || $[3] !== element || $[4] !== isClientRender || $[5] !== portalContainerName) {
|
|
53
|
+
t2 = () => {
|
|
54
|
+
var _onMountRef$current;
|
|
55
|
+
if (!element || !isClientRender) return;
|
|
56
|
+
let containerName = _containerName !== null && _containerName !== void 0 ? _containerName : portalContainerName;
|
|
57
|
+
if (containerName === void 0) {
|
|
58
|
+
containerName = DEFAULT_PORTAL_CONTAINER_NAME;
|
|
59
|
+
ensureDefaultPortal();
|
|
60
|
+
}
|
|
61
|
+
const parentElement = getPortalRoot(containerName);
|
|
62
|
+
if (!parentElement) throw new Error(`Portal container '${containerName}' is not yet registered. Container must be registered with registerPortalRoot before use.`);
|
|
63
|
+
parentElement.appendChild(element);
|
|
64
|
+
(_onMountRef$current = onMountRef.current) === null || _onMountRef$current === void 0 || _onMountRef$current.call(onMountRef);
|
|
65
|
+
return () => {
|
|
66
|
+
parentElement.removeChild(element);
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
t3 = [
|
|
70
|
+
element,
|
|
71
|
+
isClientRender,
|
|
72
|
+
_containerName,
|
|
73
|
+
portalContainerName
|
|
74
|
+
];
|
|
75
|
+
$[2] = _containerName;
|
|
76
|
+
$[3] = element;
|
|
77
|
+
$[4] = isClientRender;
|
|
78
|
+
$[5] = portalContainerName;
|
|
79
|
+
$[6] = t2;
|
|
80
|
+
$[7] = t3;
|
|
81
|
+
} else {
|
|
82
|
+
t2 = $[6];
|
|
83
|
+
t3 = $[7];
|
|
84
|
+
}
|
|
85
|
+
useIsomorphicLayoutEffect(t2, t3);
|
|
86
|
+
let t4;
|
|
87
|
+
if ($[8] !== children || $[9] !== element || $[10] !== isClientRender) {
|
|
88
|
+
t4 = element && isClientRender ? /*#__PURE__*/ createPortal(children, element) : null;
|
|
89
|
+
$[8] = children;
|
|
90
|
+
$[9] = element;
|
|
91
|
+
$[10] = isClientRender;
|
|
92
|
+
$[11] = t4;
|
|
93
|
+
} else t4 = $[11];
|
|
94
|
+
return t4;
|
|
41
95
|
};
|
|
96
|
+
function _temp() {
|
|
97
|
+
if (typeof document === "undefined") return null;
|
|
98
|
+
const div = document.createElement("div");
|
|
99
|
+
div.setAttribute("data-component", "Portal");
|
|
100
|
+
div.style.position = "relative";
|
|
101
|
+
div.style.zIndex = "1";
|
|
102
|
+
return div;
|
|
103
|
+
}
|
|
42
104
|
//#endregion
|
|
43
105
|
export { Portal };
|
|
@@ -1,68 +1,83 @@
|
|
|
1
|
+
import { c } from "react-compiler-runtime";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import { iterateFocusableElements } from "@primer/behaviors/utils";
|
|
3
4
|
//#region src/hooks/useMenuInitialFocus.ts
|
|
4
5
|
const useMenuInitialFocus = (open, containerRef, anchorRef) => {
|
|
5
|
-
|
|
6
|
-
* We need to pick the first element to focus based on how the menu was opened,
|
|
7
|
-
* however, we need to wait for the menu to be open to set focus.
|
|
8
|
-
* This is why we use set openingKey in state and have 2 effects
|
|
9
|
-
*/
|
|
6
|
+
const $ = c(9);
|
|
10
7
|
const [openingGesture, setOpeningGesture] = React.useState(void 0);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
let t0;
|
|
9
|
+
let t1;
|
|
10
|
+
if ($[0] !== anchorRef) {
|
|
11
|
+
t0 = function inferOpeningKey() {
|
|
12
|
+
const anchorElement = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
|
|
13
|
+
const clickHandler = (event) => {
|
|
14
|
+
if (event.detail !== 0) setOpeningGesture("mouse-click");
|
|
15
|
+
};
|
|
16
|
+
const keydownHandler = (event_0) => {
|
|
17
|
+
if ([
|
|
18
|
+
"Space",
|
|
19
|
+
"Enter",
|
|
20
|
+
"ArrowDown",
|
|
21
|
+
"ArrowUp"
|
|
22
|
+
].includes(event_0.code)) setOpeningGesture(event_0.code);
|
|
23
|
+
};
|
|
24
|
+
anchorElement === null || anchorElement === void 0 || anchorElement.addEventListener("click", clickHandler);
|
|
25
|
+
anchorElement === null || anchorElement === void 0 || anchorElement.addEventListener("keydown", keydownHandler);
|
|
26
|
+
return () => {
|
|
27
|
+
anchorElement === null || anchorElement === void 0 || anchorElement.removeEventListener("click", clickHandler);
|
|
28
|
+
anchorElement === null || anchorElement === void 0 || anchorElement.removeEventListener("keydown", keydownHandler);
|
|
29
|
+
};
|
|
15
30
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
t1 = [anchorRef];
|
|
32
|
+
$[0] = anchorRef;
|
|
33
|
+
$[1] = t0;
|
|
34
|
+
$[2] = t1;
|
|
35
|
+
} else {
|
|
36
|
+
t0 = $[1];
|
|
37
|
+
t1 = $[2];
|
|
38
|
+
}
|
|
39
|
+
React.useEffect(t0, t1);
|
|
40
|
+
let t2;
|
|
41
|
+
let t3;
|
|
42
|
+
if ($[3] !== anchorRef || $[4] !== containerRef || $[5] !== open || $[6] !== openingGesture) {
|
|
43
|
+
t2 = function moveFocusOnOpen() {
|
|
44
|
+
if (!open || !(containerRef !== null && containerRef !== void 0 && containerRef.current)) return;
|
|
45
|
+
const iterable = iterateFocusableElements(containerRef.current);
|
|
46
|
+
if (openingGesture === "mouse-click") if (anchorRef !== null && anchorRef !== void 0 && anchorRef.current) anchorRef.current.focus();
|
|
47
|
+
else throw new Error("For focus management, please attach anchorRef");
|
|
48
|
+
else if (openingGesture && [
|
|
20
49
|
"ArrowDown",
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
"Space",
|
|
51
|
+
"Enter"
|
|
52
|
+
].includes(openingGesture)) {
|
|
53
|
+
const firstElement = iterable.next().value;
|
|
54
|
+
setTimeout(() => firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus());
|
|
55
|
+
} else if ("ArrowUp" === openingGesture) {
|
|
56
|
+
const elements = [...iterable];
|
|
57
|
+
const lastElement = elements[elements.length - 1];
|
|
58
|
+
setTimeout(() => lastElement.focus());
|
|
59
|
+
} else {
|
|
60
|
+
const firstElement_0 = iterable.next().value;
|
|
61
|
+
setTimeout(() => firstElement_0 === null || firstElement_0 === void 0 ? void 0 : firstElement_0.focus());
|
|
62
|
+
}
|
|
29
63
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const firstElement = iterable.next().value;
|
|
48
|
-
/** We push imperative focus to the next tick to prevent React's batching */
|
|
49
|
-
setTimeout(() => firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus());
|
|
50
|
-
} else if ("ArrowUp" === openingGesture) {
|
|
51
|
-
const elements = [...iterable];
|
|
52
|
-
const lastElement = elements[elements.length - 1];
|
|
53
|
-
setTimeout(() => lastElement.focus());
|
|
54
|
-
} else {
|
|
55
|
-
/** if the menu was not opened with the anchor, we default to the first element
|
|
56
|
-
* for example: with keyboard shortcut (see stories/fixtures)
|
|
57
|
-
*/
|
|
58
|
-
const firstElement = iterable.next().value;
|
|
59
|
-
setTimeout(() => firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus());
|
|
60
|
-
}
|
|
61
|
-
}, [
|
|
62
|
-
open,
|
|
63
|
-
openingGesture,
|
|
64
|
-
anchorRef
|
|
65
|
-
]);
|
|
64
|
+
t3 = [
|
|
65
|
+
open,
|
|
66
|
+
openingGesture,
|
|
67
|
+
anchorRef,
|
|
68
|
+
containerRef
|
|
69
|
+
];
|
|
70
|
+
$[3] = anchorRef;
|
|
71
|
+
$[4] = containerRef;
|
|
72
|
+
$[5] = open;
|
|
73
|
+
$[6] = openingGesture;
|
|
74
|
+
$[7] = t2;
|
|
75
|
+
$[8] = t3;
|
|
76
|
+
} else {
|
|
77
|
+
t2 = $[7];
|
|
78
|
+
t3 = $[8];
|
|
79
|
+
}
|
|
80
|
+
React.useEffect(t2, t3);
|
|
66
81
|
};
|
|
67
82
|
//#endregion
|
|
68
83
|
export { useMenuInitialFocus };
|
package/package.json
CHANGED