@primer/react 38.26.0-rc.73c77c7d0 → 38.26.0-rc.d5cb2b340
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
CHANGED
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
|
|
37
37
|
- [#7878](https://github.com/primer/react/pull/7878) [`8c468fd`](https://github.com/primer/react/commit/8c468fd28322456f48601f9cbf6226fc4c35b661) Thanks [@mattcosta7](https://github.com/mattcosta7)! - FilteredActionList: Guard against `undefined` items in the virtualizer's `getItemKey` callback to prevent a crash when `@tanstack/react-virtual` invokes it with an index whose item was just removed (e.g. when filtering shrinks the items list).
|
|
38
38
|
|
|
39
|
+
- [#7876](https://github.com/primer/react/pull/7876) [`980e94c`](https://github.com/primer/react/commit/980e94cc1de7807bb2b3fc4dd006ea8dbf3e8303) Thanks [@mattcosta7](https://github.com/mattcosta7)! - UnderlinePanels: Eliminate the empty-tablist frame on mount and the cascading
|
|
40
|
+
re-render when icons toggle. Tabs and panels are now derived in render
|
|
41
|
+
(previously stored in state synced via `useEffect`), the list width is kept
|
|
42
|
+
in a ref instead of state, and `iconsVisible` / `loadingCounters` flow to
|
|
43
|
+
each tab via context — combined with `React.memo(Tab)`, that makes
|
|
44
|
+
resize-driven icon toggles update only the part of each tab that depends on
|
|
45
|
+
the change, not the whole tablist subtree. Behavior is unchanged.
|
|
46
|
+
|
|
39
47
|
- [#7874](https://github.com/primer/react/pull/7874) [`8cc7e99`](https://github.com/primer/react/commit/8cc7e998d2dbde1fb927b598755810b534702a6a) Thanks [@mattcosta7](https://github.com/mattcosta7)! - Dev-only effects (the `if (__DEV__) { useEffect(...) }` pattern with an
|
|
40
48
|
`eslint-disable react-hooks/rules-of-hooks` comment at every call site) are
|
|
41
49
|
now expressed via a new internal `useDevOnlyEffect` hook. The lint
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UnderlinePanels.d.ts","sourceRoot":"","sources":["../../../src/experimental/UnderlinePanels/UnderlinePanels.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"UnderlinePanels.d.ts","sourceRoot":"","sources":["../../../src/experimental/UnderlinePanels/UnderlinePanels.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAQZ,KAAK,EAAE,EACP,KAAK,iBAAiB,EAGvB,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAA;AAerD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAA;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,YAAY,CAAC,CAAA;IACjD;;OAEG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAA;IAC3D;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,EAAE,CAAC,EAAE,KAAK,CAAC,WAAW,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,iBAAiB,CAAC;IACvC;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,iBAAiB,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACxG;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;OAEG;IACH,IAAI,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAA;CACrB,CAAC,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAA;;;;;AA8M7D,wBAA2D"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c } from 'react-compiler-runtime';
|
|
2
|
-
import React, { useState, useRef,
|
|
2
|
+
import React, { useState, useRef, useMemo, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
|
|
3
3
|
import { TabContainerElement } from '@github/tab-container-element';
|
|
4
4
|
import { createComponent } from '../../utils/create-component.js';
|
|
5
5
|
import { UnderlineWrapper, UnderlineItemList, UnderlineItem } from '../../internal/components/UnderlineTabbedInterface.js';
|
|
@@ -9,10 +9,21 @@ import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect.js'
|
|
|
9
9
|
import classes from './UnderlinePanels.module.css.js';
|
|
10
10
|
import { clsx } from 'clsx';
|
|
11
11
|
import { isSlot } from '../../utils/is-slot.js';
|
|
12
|
-
import {
|
|
12
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
13
13
|
import { useId } from '../../hooks/useId.js';
|
|
14
14
|
|
|
15
15
|
const TabContainerComponent = createComponent(TabContainerElement, 'tab-container');
|
|
16
|
+
|
|
17
|
+
// Carries flags that affect every Tab's rendering but that don't belong on the
|
|
18
|
+
// consumer-facing Tab API. Passing them via context (instead of cloneElement)
|
|
19
|
+
// keeps each Tab element's props referentially stable across UnderlinePanels
|
|
20
|
+
// re-renders, so React.memo(Tab) can skip work when an unrelated piece of
|
|
21
|
+
// state changes.
|
|
22
|
+
|
|
23
|
+
const UnderlinePanelsContext = /*#__PURE__*/createContext({
|
|
24
|
+
iconsVisible: true,
|
|
25
|
+
loadingCounters: undefined
|
|
26
|
+
});
|
|
16
27
|
const UnderlinePanels = ({
|
|
17
28
|
'aria-label': ariaLabel,
|
|
18
29
|
'aria-labelledby': ariaLabelledBy,
|
|
@@ -27,21 +38,22 @@ const UnderlinePanels = ({
|
|
|
27
38
|
// We need to always call useId() because React Hooks must be
|
|
28
39
|
// called in the exact same order in every component render
|
|
29
40
|
const parentId = useId(props.id);
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
//
|
|
41
|
+
const [tabs_0, tabPanels_0, tabsHaveIcons_0] = useMemo(() => {
|
|
42
|
+
// Walk children, clone each Tab with a generated id, and each Panel with a
|
|
43
|
+
// matching aria-labelledby. Derive in render so we never ship a
|
|
44
|
+
// "before-the-effect-ran" empty-tablist frame and so that re-renders of
|
|
45
|
+
// UnderlinePanels don't churn through an extra commit cycle.
|
|
46
|
+
//
|
|
47
|
+
// iconsVisible / loadingCounters are NOT baked into the cloned Tab
|
|
48
|
+
// elements — they flow through UnderlinePanelsContext, so this memo's deps
|
|
49
|
+
// can stay tight ([children, parentId]) and Tab elements stay
|
|
50
|
+
// referentially stable across resize-driven iconsVisible toggles.
|
|
37
51
|
let tabIndex = 0;
|
|
38
52
|
let panelIndex = 0;
|
|
39
53
|
const childrenWithProps = Children.map(children, child => {
|
|
40
54
|
if (/*#__PURE__*/isValidElement(child) && (child.type === Tab || isSlot(child, Tab))) {
|
|
41
55
|
return /*#__PURE__*/cloneElement(child, {
|
|
42
|
-
id: `${parentId}-tab-${tabIndex++}
|
|
43
|
-
loadingCounters,
|
|
44
|
-
iconsVisible
|
|
56
|
+
id: `${parentId}-tab-${tabIndex++}`
|
|
45
57
|
});
|
|
46
58
|
}
|
|
47
59
|
if (/*#__PURE__*/isValidElement(child) && (child.type === Panel || isSlot(child, Panel))) {
|
|
@@ -52,64 +64,81 @@ const UnderlinePanels = ({
|
|
|
52
64
|
}
|
|
53
65
|
return child;
|
|
54
66
|
});
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}, [children, parentId
|
|
64
|
-
const
|
|
67
|
+
const tabs = [];
|
|
68
|
+
const tabPanels = [];
|
|
69
|
+
for (const child_0 of Children.toArray(childrenWithProps)) {
|
|
70
|
+
if (! /*#__PURE__*/isValidElement(child_0)) continue;
|
|
71
|
+
if (child_0.type === Tab || isSlot(child_0, Tab)) tabs.push(child_0);else if (child_0.type === Panel || isSlot(child_0, Panel)) tabPanels.push(child_0);
|
|
72
|
+
}
|
|
73
|
+
const tabsHaveIcons = tabs.some(tab => /*#__PURE__*/React.isValidElement(tab) && tab.props.icon);
|
|
74
|
+
return [tabs, tabPanels, tabsHaveIcons];
|
|
75
|
+
}, [children, parentId]);
|
|
76
|
+
const contextValue = useMemo(() => ({
|
|
77
|
+
iconsVisible,
|
|
78
|
+
loadingCounters
|
|
79
|
+
}), [iconsVisible, loadingCounters]);
|
|
65
80
|
|
|
66
|
-
//
|
|
67
|
-
|
|
81
|
+
// Mirror iconsVisible into a ref so the list observer below can read it
|
|
82
|
+
// without being re-created on every toggle (re-creating the observer
|
|
83
|
+
// would re-trigger its initial callback and churn extra work).
|
|
84
|
+
const iconsVisibleRef = useRef(iconsVisible);
|
|
68
85
|
useIsomorphicLayoutEffect(() => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
iconsVisibleRef.current = iconsVisible;
|
|
87
|
+
}, [iconsVisible]);
|
|
88
|
+
|
|
89
|
+
// The list's natural width (icons + labels), kept in sync via a
|
|
90
|
+
// ResizeObserver on the list — never read in render, so updates don't
|
|
91
|
+
// cause commits. Only refreshed while icons are visible: when icons are
|
|
92
|
+
// hidden the list is at its compressed width, which is not the value we
|
|
93
|
+
// want to compare against. The ResizeObserver fires synchronously on
|
|
94
|
+
// observe, which seeds the ref on mount for free.
|
|
95
|
+
const listWidthRef = useRef(0);
|
|
96
|
+
useResizeObserver(entries => {
|
|
97
|
+
if (!tabsHaveIcons_0) return;
|
|
98
|
+
if (!iconsVisibleRef.current) return;
|
|
99
|
+
listWidthRef.current = entries[0].contentRect.width;
|
|
100
|
+
}, listRef, []);
|
|
75
101
|
|
|
76
102
|
// when the wrapper resizes, check if the icons should be visible
|
|
77
103
|
// by comparing the wrapper width to the list width
|
|
78
104
|
useResizeObserver(resizeObserverEntries => {
|
|
79
|
-
if (!
|
|
105
|
+
if (!tabsHaveIcons_0) {
|
|
80
106
|
return;
|
|
81
107
|
}
|
|
82
108
|
const wrapperWidth = resizeObserverEntries[0].contentRect.width;
|
|
83
|
-
setIconsVisible(wrapperWidth >
|
|
109
|
+
setIconsVisible(wrapperWidth > listWidthRef.current);
|
|
84
110
|
}, wrapperRef, []);
|
|
85
111
|
if (process.env.NODE_ENV !== "production") {
|
|
86
|
-
const selectedTabs =
|
|
112
|
+
const selectedTabs = tabs_0.filter(tab_0 => {
|
|
87
113
|
const ariaSelected = /*#__PURE__*/React.isValidElement(tab_0) && tab_0.props['aria-selected'];
|
|
88
114
|
return ariaSelected === true || ariaSelected === 'true';
|
|
89
115
|
});
|
|
90
116
|
!(selectedTabs.length <= 1) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Only one tab can be selected at a time.') : invariant(false) : void 0;
|
|
91
|
-
!(
|
|
117
|
+
!(tabs_0.length === tabPanels_0.length) ? process.env.NODE_ENV !== "production" ? invariant(false, `The number of tabs and panels must be equal. Counted ${tabs_0.length} tabs and ${tabPanels_0.length} panels.`) : invariant(false) : void 0;
|
|
92
118
|
}
|
|
93
|
-
return /*#__PURE__*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
return /*#__PURE__*/jsx(UnderlinePanelsContext.Provider, {
|
|
120
|
+
value: contextValue,
|
|
121
|
+
children: /*#__PURE__*/jsxs(TabContainerComponent, {
|
|
122
|
+
children: [/*#__PURE__*/jsx(UnderlineWrapper, {
|
|
123
|
+
ref: wrapperRef,
|
|
124
|
+
slot: "tablist-wrapper",
|
|
125
|
+
"data-icons-visible": iconsVisible,
|
|
126
|
+
className: clsx(className, classes.StyledUnderlineWrapper),
|
|
127
|
+
...props,
|
|
128
|
+
children: /*#__PURE__*/jsx(UnderlineItemList, {
|
|
129
|
+
ref: listRef,
|
|
130
|
+
"aria-label": ariaLabel,
|
|
131
|
+
"aria-labelledby": ariaLabelledBy,
|
|
132
|
+
role: "tablist",
|
|
133
|
+
children: tabs_0
|
|
134
|
+
})
|
|
135
|
+
}), tabPanels_0]
|
|
136
|
+
})
|
|
108
137
|
});
|
|
109
138
|
};
|
|
110
139
|
UnderlinePanels.displayName = "UnderlinePanels";
|
|
111
|
-
const
|
|
112
|
-
const $ = c(
|
|
140
|
+
const TabImpl = t0 => {
|
|
141
|
+
const $ = c(16);
|
|
113
142
|
let ariaSelected;
|
|
114
143
|
let onSelect;
|
|
115
144
|
let props;
|
|
@@ -128,6 +157,10 @@ const Tab = t0 => {
|
|
|
128
157
|
onSelect = $[2];
|
|
129
158
|
props = $[3];
|
|
130
159
|
}
|
|
160
|
+
const {
|
|
161
|
+
iconsVisible,
|
|
162
|
+
loadingCounters
|
|
163
|
+
} = useContext(UnderlinePanelsContext);
|
|
131
164
|
let t1;
|
|
132
165
|
if ($[4] !== onSelect) {
|
|
133
166
|
t1 = event => {
|
|
@@ -156,7 +189,7 @@ const Tab = t0 => {
|
|
|
156
189
|
const keyDownHandler = t2;
|
|
157
190
|
const t3 = ariaSelected ? 0 : -1;
|
|
158
191
|
let t4;
|
|
159
|
-
if ($[8] !== ariaSelected || $[9] !== clickHandler || $[10] !==
|
|
192
|
+
if ($[8] !== ariaSelected || $[9] !== clickHandler || $[10] !== iconsVisible || $[11] !== keyDownHandler || $[12] !== loadingCounters || $[13] !== props || $[14] !== t3) {
|
|
160
193
|
t4 = /*#__PURE__*/jsx(UnderlineItem, {
|
|
161
194
|
as: "button",
|
|
162
195
|
role: "tab",
|
|
@@ -165,19 +198,30 @@ const Tab = t0 => {
|
|
|
165
198
|
type: "button",
|
|
166
199
|
onClick: clickHandler,
|
|
167
200
|
onKeyDown: keyDownHandler,
|
|
201
|
+
iconsVisible: iconsVisible,
|
|
202
|
+
loadingCounters: loadingCounters,
|
|
168
203
|
...props
|
|
169
204
|
});
|
|
170
205
|
$[8] = ariaSelected;
|
|
171
206
|
$[9] = clickHandler;
|
|
172
|
-
$[10] =
|
|
173
|
-
$[11] =
|
|
174
|
-
$[12] =
|
|
175
|
-
$[13] =
|
|
207
|
+
$[10] = iconsVisible;
|
|
208
|
+
$[11] = keyDownHandler;
|
|
209
|
+
$[12] = loadingCounters;
|
|
210
|
+
$[13] = props;
|
|
211
|
+
$[14] = t3;
|
|
212
|
+
$[15] = t4;
|
|
176
213
|
} else {
|
|
177
|
-
t4 = $[
|
|
214
|
+
t4 = $[15];
|
|
178
215
|
}
|
|
179
216
|
return t4;
|
|
180
217
|
};
|
|
218
|
+
|
|
219
|
+
// Memoized so that UnderlinePanels re-rendering (e.g. when iconsVisible flips)
|
|
220
|
+
// only re-renders Tabs whose own props actually changed. iconsVisible and
|
|
221
|
+
// loadingCounters reach Tab via UnderlinePanelsContext, so Tabs still react
|
|
222
|
+
// to those changes through context propagation.
|
|
223
|
+
TabImpl.displayName = 'UnderlinePanels.Tab';
|
|
224
|
+
const Tab = /*#__PURE__*/React.memo(TabImpl);
|
|
181
225
|
Tab.displayName = 'UnderlinePanels.Tab';
|
|
182
226
|
const Panel = t0 => {
|
|
183
227
|
const $ = c(6);
|
package/package.json
CHANGED