@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,EAMZ,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;;;;;AA+J7D,wBAA2D"}
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, useEffect, Children, isValidElement, cloneElement } from 'react';
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 { jsxs, jsx } from 'react/jsx-runtime';
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 [tabs, setTabs] = useState([]);
31
- const [tabPanels, setTabPanels] = useState([]);
32
-
33
- // Make sure we have fresh prop data whenever the tabs or panels are updated (keep aria-selected current)
34
- useEffect(() => {
35
- // Loop through the chidren, if it's a tab, then add id="{id}-tab-{index}"
36
- // If it's a panel, then add aria-labelledby="{id}-tab-{index}"
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 newTabs = Children.toArray(childrenWithProps).filter(child_0 => {
56
- return /*#__PURE__*/isValidElement(child_0) && (child_0.type === Tab || isSlot(child_0, Tab));
57
- });
58
- const newTabPanels = Children.toArray(childrenWithProps).filter(child_1 => /*#__PURE__*/isValidElement(child_1) && (child_1.type === Panel || isSlot(child_1, Panel)));
59
-
60
- // eslint-disable-next-line react-hooks/set-state-in-effect
61
- setTabs(newTabs);
62
- setTabPanels(newTabPanels);
63
- }, [children, parentId, loadingCounters, iconsVisible]);
64
- const tabsHaveIcons = tabs.some(tab => /*#__PURE__*/React.isValidElement(tab) && tab.props.icon);
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
- // this is a workaround to get the list's width on the first render
67
- const [listWidth, setListWidth] = useState(0);
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
- var _listRef$current$getB, _listRef$current;
70
- if (!tabsHaveIcons) {
71
- return;
72
- }
73
- setListWidth((_listRef$current$getB = (_listRef$current = listRef.current) === null || _listRef$current === void 0 ? void 0 : _listRef$current.getBoundingClientRect().width) !== null && _listRef$current$getB !== void 0 ? _listRef$current$getB : 0);
74
- }, [tabsHaveIcons]);
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 (!tabsHaveIcons) {
105
+ if (!tabsHaveIcons_0) {
80
106
  return;
81
107
  }
82
108
  const wrapperWidth = resizeObserverEntries[0].contentRect.width;
83
- setIconsVisible(wrapperWidth > listWidth);
109
+ setIconsVisible(wrapperWidth > listWidthRef.current);
84
110
  }, wrapperRef, []);
85
111
  if (process.env.NODE_ENV !== "production") {
86
- const selectedTabs = tabs.filter(tab_0 => {
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
- !(tabs.length === tabPanels.length) ? process.env.NODE_ENV !== "production" ? invariant(false, `The number of tabs and panels must be equal. Counted ${tabs.length} tabs and ${tabPanels.length} panels.`) : invariant(false) : void 0;
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__*/jsxs(TabContainerComponent, {
94
- children: [/*#__PURE__*/jsx(UnderlineWrapper, {
95
- ref: wrapperRef,
96
- slot: "tablist-wrapper",
97
- "data-icons-visible": iconsVisible,
98
- className: clsx(className, classes.StyledUnderlineWrapper),
99
- ...props,
100
- children: /*#__PURE__*/jsx(UnderlineItemList, {
101
- ref: listRef,
102
- "aria-label": ariaLabel,
103
- "aria-labelledby": ariaLabelledBy,
104
- role: "tablist",
105
- children: tabs
106
- })
107
- }), tabPanels]
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 Tab = t0 => {
112
- const $ = c(14);
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] !== keyDownHandler || $[11] !== props || $[12] !== t3) {
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] = keyDownHandler;
173
- $[11] = props;
174
- $[12] = t3;
175
- $[13] = t4;
207
+ $[10] = iconsVisible;
208
+ $[11] = keyDownHandler;
209
+ $[12] = loadingCounters;
210
+ $[13] = props;
211
+ $[14] = t3;
212
+ $[15] = t4;
176
213
  } else {
177
- t4 = $[13];
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@primer/react",
3
3
  "type": "module",
4
- "version": "38.26.0-rc.73c77c7d0",
4
+ "version": "38.26.0-rc.d5cb2b340",
5
5
  "description": "An implementation of GitHub's Primer Design System using React",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",