@primer/react 38.37.0-rc.0b6dd7e38 → 38.37.0-rc.1789e76c6

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
@@ -4,8 +4,12 @@
4
4
 
5
5
  ### Minor Changes
6
6
 
7
+ - [#8306](https://github.com/primer/react/pull/8306) [`a3afd7b`](https://github.com/primer/react/commit/a3afd7baf05ea6c4a272a40ccb636f20a48f87dc) Thanks [@llastflowers](https://github.com/llastflowers)! - Deprecate the `Flash` component and move to `@primer/react/deprecated`
8
+
7
9
  - [#8295](https://github.com/primer/react/pull/8295) [`bfebb8a`](https://github.com/primer/react/commit/bfebb8a538e6f068a457722b154799876bda3ab4) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Textarea: Add an `autoSize` option that allows the field to grow with its content
8
10
 
11
+ - [#8233](https://github.com/primer/react/pull/8233) [`80a68cf`](https://github.com/primer/react/commit/80a68cf05226323ad8cbc0531f244d60ea6747e3) Thanks [@rickyzhangca](https://github.com/rickyzhangca)! - UnderlinePanels: add controlled `value`/`defaultValue`/`onChange` and `activationMode` props, keyed to a per-`Tab`/`Panel` `value`, for data-driven tabs. Behind the `primer_react_underline_panels_controlled` feature flag; the existing `aria-selected`/`onSelect` API is unchanged.
12
+
9
13
  ### Patch Changes
10
14
 
11
15
  - [#8304](https://github.com/primer/react/pull/8304) [`88feb65`](https://github.com/primer/react/commit/88feb658c13fec154da1a8aa199250514f4fdba3) Thanks [@iansan5653](https://github.com/iansan5653)! - Animate `SegmentedControl` when switching between items
@@ -8,7 +8,8 @@ const DefaultFeatureFlags = FeatureFlagScope.create({
8
8
  primer_react_action_list_group_heading_trailing_action: false,
9
9
  primer_react_action_list_item_gap: false,
10
10
  primer_react_timeline_list_semantics: false,
11
- primer_react_merged_forwarded_refs: false
11
+ primer_react_merged_forwarded_refs: false,
12
+ primer_react_underline_panels_controlled: false
12
13
  });
13
14
  //#endregion
14
15
  export { DefaultFeatureFlags };
@@ -2,11 +2,17 @@ import { ForwardRefComponent } from "../utils/polymorphic.js";
2
2
  import React from "react";
3
3
 
4
4
  //#region src/Flash/Flash.d.ts
5
+ /**
6
+ * @deprecated Use `Banner` instead. If migration is not yet possible, import `Flash` from `@primer/react/deprecated`.
7
+ */
5
8
  type FlashProps = React.ComponentPropsWithoutRef<'div'> & {
6
9
  className?: string;
7
10
  variant?: 'default' | 'warning' | 'success' | 'danger';
8
11
  full?: boolean;
9
12
  };
13
+ /**
14
+ * @deprecated Use `Banner` instead. If migration is not yet possible, import `Flash` from `@primer/react/deprecated`.
15
+ */
10
16
  declare const Flash: ForwardRefComponent<"div", FlashProps>;
11
17
  //#endregion
12
18
  export { FlashProps, Flash as default };
@@ -4,6 +4,12 @@ import { clsx } from "clsx";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  import React from "react";
6
6
  //#region src/Flash/Flash.tsx
7
+ /**
8
+ * @deprecated Use `Banner` instead. If migration is not yet possible, import `Flash` from `@primer/react/deprecated`.
9
+ */
10
+ /**
11
+ * @deprecated Use `Banner` instead. If migration is not yet possible, import `Flash` from `@primer/react/deprecated`.
12
+ */
7
13
  const Flash = /*#__PURE__*/ React.forwardRef(function Flash(t0, ref) {
8
14
  const $ = c(15);
9
15
  let className;
@@ -3,6 +3,7 @@ import { ItemProps } from "./ActionList/Item.js";
3
3
  import { GroupedListProps, ItemInput, ListProps } from "./ActionList/List.js";
4
4
  import { ActionList } from "./ActionList/index.js";
5
5
  import Octicon, { OcticonProps } from "../Octicon/Octicon.js";
6
+ import Flash, { FlashProps } from "../Flash/Flash.js";
6
7
  import { ActionMenu, ActionMenuProps } from "./ActionMenu.js";
7
8
  import FilteredSearch, { FilteredSearchProps } from "./FilteredSearch/FilteredSearch.js";
8
9
  import _default$2, { UnderlineNavLinkProps, UnderlineNavProps } from "./UnderlineNav/UnderlineNav.js";
@@ -10,4 +11,4 @@ import _default, { DialogHeaderProps, DialogProps } from "./DialogV1/Dialog.js";
10
11
  import Pagehead, { PageheadProps } from "../Pagehead/Pagehead.js";
11
12
  import _default$1, { TabNavLinkProps, TabNavProps } from "../TabNav/TabNav.js";
12
13
  import Tooltip, { TooltipProps } from "../Tooltip/Tooltip.js";
13
- export { ActionList, type GroupProps as ActionListGroupProps, type GroupedListProps as ActionListGroupedListProps, type ItemInput as ActionListItemInput, type ItemProps as ActionListItemProps, type ListProps as ActionListProps, ActionMenu, type ActionMenuProps, _default as Dialog, type DialogHeaderProps, type DialogProps, FilteredSearch, type FilteredSearchProps, Octicon, type OcticonProps, Pagehead, type PageheadProps, _default$1 as TabNav, type TabNavLinkProps, type TabNavProps, Tooltip, type TooltipProps, _default$2 as UnderlineNav, type UnderlineNavLinkProps, type UnderlineNavProps };
14
+ export { ActionList, type GroupProps as ActionListGroupProps, type GroupedListProps as ActionListGroupedListProps, type ItemInput as ActionListItemInput, type ItemProps as ActionListItemProps, type ListProps as ActionListProps, ActionMenu, type ActionMenuProps, _default as Dialog, type DialogHeaderProps, type DialogProps, FilteredSearch, type FilteredSearchProps, Flash, type FlashProps, Octicon, type OcticonProps, Pagehead, type PageheadProps, _default$1 as TabNav, type TabNavLinkProps, type TabNavProps, Tooltip, type TooltipProps, _default$2 as UnderlineNav, type UnderlineNavLinkProps, type UnderlineNavProps };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import Octicon from "../Octicon/Octicon.js";
3
+ import Flash from "../Flash/Flash.js";
3
4
  import { ActionList } from "./ActionList/index.js";
4
5
  import { ActionMenu } from "./ActionMenu.js";
5
6
  import FilteredSearch from "./FilteredSearch/FilteredSearch.js";
@@ -17,4 +18,4 @@ import Tooltip from "../Tooltip/Tooltip.js";
17
18
  * example: import {FormGroup} from '@primer/react/deprecated
18
19
  */
19
20
  //#endregion
20
- export { ActionList, ActionMenu, Dialog_default as Dialog, FilteredSearch, Octicon, Pagehead, TabNav_default as TabNav, Tooltip, UnderlineNav_default as UnderlineNav };
21
+ export { ActionList, ActionMenu, Dialog_default as Dialog, FilteredSearch, Flash, Octicon, Pagehead, TabNav_default as TabNav, Tooltip, UnderlineNav_default as UnderlineNav };
@@ -1,4 +1,5 @@
1
1
  import useIsomorphicLayoutEffect from "../../utils/useIsomorphicLayoutEffect.js";
2
+ import { useFeatureFlag } from "../../FeatureFlags/useFeatureFlag.js";
2
3
  import { useControllableState } from "../../hooks/useControllableState.js";
3
4
  import { TabsContext } from "./TabsContext.js";
4
5
  import { c } from "react-compiler-runtime";
@@ -13,11 +14,13 @@ import React, { useId } from "react";
13
14
  * The `Tab`, `TabList`, and `TabPanel` components are provided for convenience to showcase the API & implementation.
14
15
  */
15
16
  function Tabs(props) {
16
- var _props$id, _props$defaultValue;
17
- const $ = c(13);
17
+ var _props$id, _props$activationMode, _props$defaultValue;
18
+ const $ = c(16);
18
19
  const { children, onValueChange } = props;
19
20
  const generatedId = useId();
20
21
  const groupId = (_props$id = props.id) !== null && _props$id !== void 0 ? _props$id : generatedId;
22
+ const controlledApiEnabled = useFeatureFlag("primer_react_underline_panels_controlled");
23
+ const activationMode = controlledApiEnabled ? (_props$activationMode = props.activationMode) !== null && _props$activationMode !== void 0 ? _props$activationMode : "automatic" : "automatic";
21
24
  const t0 = (_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : props.value;
22
25
  let t1;
23
26
  if ($[0] !== props.value || $[1] !== t0) {
@@ -31,49 +34,60 @@ function Tabs(props) {
31
34
  $[2] = t1;
32
35
  } else t1 = $[2];
33
36
  const [selectedValue, setSelectedValue] = useControllableState(t1);
37
+ const [focusedValue, setFocusedValue] = React.useState();
34
38
  const savedOnValueChange = React.useRef(onValueChange);
35
39
  let t2;
36
- if ($[3] !== groupId || $[4] !== selectedValue || $[5] !== setSelectedValue) {
40
+ if ($[3] !== activationMode || $[4] !== controlledApiEnabled || $[5] !== focusedValue || $[6] !== groupId || $[7] !== selectedValue || $[8] !== setSelectedValue) {
37
41
  t2 = {
38
42
  groupId,
39
43
  selectedValue,
44
+ activationMode,
45
+ focusedValue,
40
46
  selectTab(value) {
41
47
  var _savedOnValueChange$c;
48
+ setFocusedValue(void 0);
49
+ if (controlledApiEnabled && value === selectedValue) return;
42
50
  setSelectedValue(value);
43
51
  (_savedOnValueChange$c = savedOnValueChange.current) === null || _savedOnValueChange$c === void 0 || _savedOnValueChange$c.call(savedOnValueChange, { value });
52
+ },
53
+ focusTab(value_0) {
54
+ setFocusedValue(value_0);
44
55
  }
45
56
  };
46
- $[3] = groupId;
47
- $[4] = selectedValue;
48
- $[5] = setSelectedValue;
49
- $[6] = t2;
50
- } else t2 = $[6];
57
+ $[3] = activationMode;
58
+ $[4] = controlledApiEnabled;
59
+ $[5] = focusedValue;
60
+ $[6] = groupId;
61
+ $[7] = selectedValue;
62
+ $[8] = setSelectedValue;
63
+ $[9] = t2;
64
+ } else t2 = $[9];
51
65
  const contextValue = t2;
52
66
  let t3;
53
67
  let t4;
54
- if ($[7] !== onValueChange) {
68
+ if ($[10] !== onValueChange) {
55
69
  t3 = () => {
56
70
  savedOnValueChange.current = onValueChange;
57
71
  };
58
72
  t4 = [onValueChange];
59
- $[7] = onValueChange;
60
- $[8] = t3;
61
- $[9] = t4;
73
+ $[10] = onValueChange;
74
+ $[11] = t3;
75
+ $[12] = t4;
62
76
  } else {
63
- t3 = $[8];
64
- t4 = $[9];
77
+ t3 = $[11];
78
+ t4 = $[12];
65
79
  }
66
80
  useIsomorphicLayoutEffect(t3, t4);
67
81
  let t5;
68
- if ($[10] !== children || $[11] !== contextValue) {
82
+ if ($[13] !== children || $[14] !== contextValue) {
69
83
  t5 = /*#__PURE__*/ jsx(TabsContext.Provider, {
70
84
  value: contextValue,
71
85
  children
72
86
  });
73
- $[10] = children;
74
- $[11] = contextValue;
75
- $[12] = t5;
76
- } else t5 = $[12];
87
+ $[13] = children;
88
+ $[14] = contextValue;
89
+ $[15] = t5;
90
+ } else t5 = $[15];
77
91
  return t5;
78
92
  }
79
93
  //#endregion
@@ -51,6 +51,12 @@ type CommonTabsProps = {
51
51
  * unique id is generated automatically.
52
52
  */
53
53
  id?: string;
54
+ /**
55
+ * `'automatic'` (default) selects on focus, so Arrow/Home/End keys select immediately. `'manual'`
56
+ * only moves focus; selection commits on Enter/Space/click. Prefer `'manual'` when displaying a
57
+ * panel is not instant (e.g. it triggers a network request).
58
+ */
59
+ activationMode?: 'automatic' | 'manual';
54
60
  };
55
61
  type TabsProps = PropsWithChildren<(ControlledTabsProps | UncontrolledTabsProps) & CommonTabsProps>;
56
62
  type Label = {
@@ -12,6 +12,7 @@ function useTab(props) {
12
12
  const selected = tabs.selectedValue === value;
13
13
  const id = `${tabs.groupId}-tab-${value}`;
14
14
  const panelId = `${tabs.groupId}-panel-${value}`;
15
+ const isTabStop = tabs.activationMode === "manual" && tabs.focusedValue !== void 0 ? tabs.focusedValue === value : selected;
15
16
  let t0;
16
17
  if ($[0] !== tabs || $[1] !== value) {
17
18
  t0 = function onKeyDown(event) {
@@ -37,6 +38,10 @@ function useTab(props) {
37
38
  let t2;
38
39
  if ($[7] !== disabled || $[8] !== selected || $[9] !== tabs || $[10] !== value) {
39
40
  t2 = function onFocus() {
41
+ if (tabs.activationMode === "manual") {
42
+ if (!disabled) tabs.focusTab(value);
43
+ return;
44
+ }
40
45
  if (!selected && !disabled) tabs.selectTab(value);
41
46
  };
42
47
  $[7] = disabled;
@@ -47,7 +52,7 @@ function useTab(props) {
47
52
  } else t2 = $[11];
48
53
  const onFocus = t2;
49
54
  const t3 = disabled ? true : void 0;
50
- const t4 = selected ? 0 : -1;
55
+ const t4 = isTabStop ? 0 : -1;
51
56
  let t5;
52
57
  if ($[12] !== id || $[13] !== onFocus || $[14] !== onKeyDown || $[15] !== onMouseDown || $[16] !== panelId || $[17] !== selected || $[18] !== t3 || $[19] !== t4) {
53
58
  t5 = { tabProps: {
@@ -5,20 +5,29 @@ import { c } from "react-compiler-runtime";
5
5
  import { useRef } from "react";
6
6
  //#region src/experimental/Tabs/useTabList.ts
7
7
  function useTabList(props) {
8
- const $ = c(9);
8
+ const $ = c(10);
9
9
  const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-orientation": ariaOrientation } = props;
10
10
  const mergedRefEnabled = useFeatureFlag("primer_react_merged_forwarded_refs");
11
+ const controlledApiEnabled = useFeatureFlag("primer_react_underline_panels_controlled");
11
12
  const tabListRef = useRef(null);
12
13
  const mergedRef = useMergedRefs(tabListRef, props.ref);
13
14
  const providedOrCreatedRef = useProvidedRefOrCreate(props.ref);
14
15
  const readRef = mergedRefEnabled ? tabListRef : providedOrCreatedRef;
15
16
  const appliedRef = mergedRefEnabled ? mergedRef : providedOrCreatedRef;
16
17
  let t0;
17
- if ($[0] !== ariaOrientation || $[1] !== readRef) {
18
+ if ($[0] !== ariaOrientation || $[1] !== controlledApiEnabled || $[2] !== readRef) {
18
19
  t0 = (event) => {
19
20
  const { current: tablist } = readRef;
20
21
  if (tablist === null) return;
21
22
  const tabs = getFocusableTabs(tablist);
23
+ const getCurrentIndex = () => {
24
+ if (controlledApiEnabled) {
25
+ const activeElement = tablist.ownerDocument.activeElement;
26
+ const focusedIndex = activeElement instanceof HTMLElement ? tabs.indexOf(activeElement) : -1;
27
+ if (focusedIndex !== -1) return focusedIndex;
28
+ }
29
+ return tabs.findIndex(_temp);
30
+ };
22
31
  const isVertical = ariaOrientation === "vertical";
23
32
  const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
24
33
  const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
@@ -27,13 +36,13 @@ function useTabList(props) {
27
36
  event.stopPropagation();
28
37
  }
29
38
  if (event.key === nextKey) {
30
- const selectedTabIndex = tabs.findIndex(_temp);
31
- if (selectedTabIndex === -1) return;
32
- tabs[(selectedTabIndex + 1) % tabs.length].focus();
39
+ const currentTabIndex = getCurrentIndex();
40
+ if (currentTabIndex === -1) return;
41
+ tabs[(currentTabIndex + 1) % tabs.length].focus();
33
42
  } else if (event.key === prevKey) {
34
- const selectedTabIndex_0 = tabs.findIndex(_temp2);
35
- if (selectedTabIndex_0 === -1) return;
36
- tabs[(tabs.length + selectedTabIndex_0 - 1) % tabs.length].focus();
43
+ const currentTabIndex_0 = getCurrentIndex();
44
+ if (currentTabIndex_0 === -1) return;
45
+ tabs[(tabs.length + currentTabIndex_0 - 1) % tabs.length].focus();
37
46
  } else if (event.key === "Home") {
38
47
  if (tabs[0]) tabs[0].focus();
39
48
  } else if (event.key === "End") {
@@ -41,13 +50,14 @@ function useTabList(props) {
41
50
  }
42
51
  };
43
52
  $[0] = ariaOrientation;
44
- $[1] = readRef;
45
- $[2] = t0;
46
- } else t0 = $[2];
53
+ $[1] = controlledApiEnabled;
54
+ $[2] = readRef;
55
+ $[3] = t0;
56
+ } else t0 = $[3];
47
57
  const onKeyDown = t0;
48
58
  const t1 = ariaOrientation !== null && ariaOrientation !== void 0 ? ariaOrientation : "horizontal";
49
59
  let t2;
50
- if ($[3] !== appliedRef || $[4] !== ariaLabel || $[5] !== ariaLabelledby || $[6] !== onKeyDown || $[7] !== t1) {
60
+ if ($[4] !== appliedRef || $[5] !== ariaLabel || $[6] !== ariaLabelledby || $[7] !== onKeyDown || $[8] !== t1) {
51
61
  t2 = { tabListProps: {
52
62
  ref: appliedRef,
53
63
  "aria-label": ariaLabel,
@@ -56,18 +66,15 @@ function useTabList(props) {
56
66
  role: "tablist",
57
67
  onKeyDown
58
68
  } };
59
- $[3] = appliedRef;
60
- $[4] = ariaLabel;
61
- $[5] = ariaLabelledby;
62
- $[6] = onKeyDown;
63
- $[7] = t1;
64
- $[8] = t2;
65
- } else t2 = $[8];
69
+ $[4] = appliedRef;
70
+ $[5] = ariaLabel;
71
+ $[6] = ariaLabelledby;
72
+ $[7] = onKeyDown;
73
+ $[8] = t1;
74
+ $[9] = t2;
75
+ } else t2 = $[9];
66
76
  return t2;
67
77
  }
68
- function _temp2(tab_0) {
69
- return tab_0.getAttribute("aria-selected") === "true";
70
- }
71
78
  function _temp(tab) {
72
79
  return tab.getAttribute("aria-selected") === "true";
73
80
  }
@@ -16,6 +16,31 @@ type UnderlinePanelsProps = {
16
16
  * ID of the element containing the name for the tab list
17
17
  */
18
18
  'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
19
+ /**
20
+ * The value of the selected tab, keyed to each `UnderlinePanels.Tab`/`UnderlinePanels.Panel`
21
+ * `value`. Provide this (with `onChange`) for a controlled component where the selected tab is
22
+ * the single source of truth.
23
+ */
24
+ value?: string;
25
+ /**
26
+ * The value of the tab that should be selected by default, keyed to each tab's `value`. Use this
27
+ * for an uncontrolled component. Cannot be combined with `value`.
28
+ */
29
+ defaultValue?: string;
30
+ /**
31
+ * Fires whenever the selected tab changes, for every selection method including arrow keys.
32
+ * Unlike `UnderlinePanels.Tab`'s `onSelect`, which only fires on click and Enter/Space.
33
+ */
34
+ onChange?: ({
35
+ value
36
+ }: {
37
+ value: string;
38
+ }) => void;
39
+ /**
40
+ * `'automatic'` (default) selects on focus/arrow keys; `'manual'` only moves focus until
41
+ * Enter/Space/click. Prefer `'manual'` when displaying a panel is not instant.
42
+ */
43
+ activationMode?: 'automatic' | 'manual';
19
44
  /**
20
45
  * Custom string to use when generating the IDs of tabs and `aria-labelledby` for the panels
21
46
  */
@@ -34,6 +59,12 @@ type UnderlinePanelsProps = {
34
59
  as?: React.ElementType;
35
60
  };
36
61
  type TabProps = PropsWithChildren<{
62
+ /**
63
+ * A value that uniquely identifies this tab, paired with the `UnderlinePanels.Panel` of the same
64
+ * `value`. Provide this to use domain values (e.g. `'branch'`) with the container's controlled
65
+ * `value`/`onChange` API. When omitted, tabs and panels are paired by their DOM order.
66
+ */
67
+ value?: string;
37
68
  /**
38
69
  * Whether this is the selected tab
39
70
  */
@@ -51,7 +82,13 @@ type TabProps = PropsWithChildren<{
51
82
  */
52
83
  icon?: FC<IconProps>;
53
84
  }>;
54
- type PanelProps = React.HTMLAttributes<HTMLDivElement>;
85
+ type PanelProps = React.HTMLAttributes<HTMLDivElement> & {
86
+ /**
87
+ * A value that uniquely identifies this panel, paired with the `UnderlinePanels.Tab` of the same
88
+ * `value`. When omitted, tabs and panels are paired by their DOM order.
89
+ */
90
+ value?: string;
91
+ };
55
92
  declare const _default: React.FC<UnderlinePanelsProps> & SlotMarker & {
56
93
  Panel: FCWithSlotMarker<PanelProps>;
57
94
  Tab: FCWithSlotMarker<TabProps>;
@@ -1,7 +1,9 @@
1
1
  import useIsomorphicLayoutEffect from "../../utils/useIsomorphicLayoutEffect.js";
2
2
  import { useResizeObserver } from "../../hooks/useResizeObserver.js";
3
3
  import { useId as useId$1 } from "../../hooks/useId.js";
4
+ import { warning } from "../../utils/warning.js";
4
5
  import { isSlot } from "../../utils/is-slot.js";
6
+ import { useFeatureFlag } from "../../FeatureFlags/useFeatureFlag.js";
5
7
  import { invariant } from "../../utils/invariant.js";
6
8
  import { UnderlineItem, UnderlineItemList, UnderlineWrapper } from "../../internal/components/UnderlineTabbedInterface.js";
7
9
  import UnderlinePanels_module_css_default from "./UnderlinePanels.module.css.js";
@@ -17,43 +19,78 @@ const UnderlinePanelsContext = /*#__PURE__*/ createContext({
17
19
  iconsVisible: true,
18
20
  loadingCounters: void 0
19
21
  });
20
- const UnderlinePanels = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, loadingCounters, className, ...props }) => {
22
+ const UnderlinePanels = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, loadingCounters, className, value: valueProp, defaultValue: defaultValueProp, onChange: onChangeProp, activationMode = "automatic", ...props }) => {
23
+ var _tabValues$;
24
+ const controlledApiEnabled = useFeatureFlag("primer_react_underline_panels_controlled");
25
+ const controlledValue = controlledApiEnabled ? valueProp : void 0;
26
+ const defaultValue = controlledApiEnabled ? defaultValueProp : void 0;
27
+ const onChange = controlledApiEnabled ? onChangeProp : void 0;
21
28
  const [iconsVisible, setIconsVisible] = useState(true);
22
29
  const wrapperRef = useRef(null);
23
30
  const listRef = useRef(null);
24
31
  const parentId = useId$1(props.id);
25
- const [tabs, tabPanels, tabsHaveIcons, selectedFromProps] = useMemo(() => {
32
+ const [tabs, tabPanels, tabsHaveIcons, selectedFromProps, tabValues, panelValues] = useMemo(() => {
26
33
  let tabIndex = 0;
27
34
  let panelIndex = 0;
28
35
  const childrenWithProps = Children.map(children, (child) => {
29
- if (/*#__PURE__*/ isValidElement(child) && (child.type === Tab || isSlot(child, Tab))) return /*#__PURE__*/ cloneElement(child, { value: `${tabIndex++}` });
30
- if (/*#__PURE__*/ isValidElement(child) && (child.type === Panel || isSlot(child, Panel))) return /*#__PURE__*/ cloneElement(child, { value: `${panelIndex++}` });
36
+ if (/*#__PURE__*/ isValidElement(child) && (child.type === Tab || isSlot(child, Tab))) {
37
+ var _ref;
38
+ const value = (_ref = controlledApiEnabled ? child.props.value : void 0) !== null && _ref !== void 0 ? _ref : `${tabIndex}`;
39
+ tabIndex++;
40
+ return /*#__PURE__*/ cloneElement(child, { value });
41
+ }
42
+ if (/*#__PURE__*/ isValidElement(child) && (child.type === Panel || isSlot(child, Panel))) {
43
+ var _ref2;
44
+ const value = (_ref2 = controlledApiEnabled ? child.props.value : void 0) !== null && _ref2 !== void 0 ? _ref2 : `${panelIndex}`;
45
+ panelIndex++;
46
+ return /*#__PURE__*/ cloneElement(child, { value });
47
+ }
31
48
  return child;
32
49
  });
33
50
  const tabs = [];
34
51
  const tabPanels = [];
52
+ const tabValues = [];
53
+ const panelValues = [];
35
54
  let selectedFromProps;
36
55
  for (const child of Children.toArray(childrenWithProps)) {
37
56
  if (!/*#__PURE__*/ isValidElement(child)) continue;
38
57
  if (child.type === Tab || isSlot(child, Tab)) {
58
+ const value = child.props.value;
59
+ if (value !== void 0) tabValues.push(value);
39
60
  const ariaSelected = child.props["aria-selected"];
40
- if (ariaSelected === true || ariaSelected === "true") selectedFromProps = `${tabs.length}`;
61
+ if (ariaSelected === true || ariaSelected === "true") selectedFromProps = value;
41
62
  tabs.push(child);
42
- } else if (child.type === Panel || isSlot(child, Panel)) tabPanels.push(child);
63
+ } else if (child.type === Panel || isSlot(child, Panel)) {
64
+ const value = child.props.value;
65
+ if (value !== void 0) panelValues.push(value);
66
+ tabPanels.push(child);
67
+ }
43
68
  }
44
69
  return [
45
70
  tabs,
46
71
  tabPanels,
47
72
  tabs.some((tab) => /*#__PURE__*/ React.isValidElement(tab) && tab.props.icon),
48
- selectedFromProps
73
+ selectedFromProps,
74
+ tabValues,
75
+ panelValues
49
76
  ];
50
- }, [children]);
51
- const [selectedValue, setSelectedValue] = useState(() => selectedFromProps !== null && selectedFromProps !== void 0 ? selectedFromProps : "0");
77
+ }, [children, controlledApiEnabled]);
78
+ const isControlled = controlledValue !== void 0;
79
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => {
80
+ var _ref3;
81
+ return (_ref3 = defaultValue !== null && defaultValue !== void 0 ? defaultValue : selectedFromProps) !== null && _ref3 !== void 0 ? _ref3 : "0";
82
+ });
52
83
  const [prevSelectedFromProps, setPrevSelectedFromProps] = useState(selectedFromProps);
53
- if (selectedFromProps !== prevSelectedFromProps) {
84
+ if (!isControlled && selectedFromProps !== prevSelectedFromProps) {
54
85
  setPrevSelectedFromProps(selectedFromProps);
55
- if (selectedFromProps !== void 0 && selectedFromProps !== selectedValue) setSelectedValue(selectedFromProps);
86
+ if (selectedFromProps !== void 0 && selectedFromProps !== uncontrolledValue) setUncontrolledValue(selectedFromProps);
56
87
  }
88
+ const selectedValue = isControlled ? controlledValue : uncontrolledValue;
89
+ const effectiveValue = tabValues.includes(selectedValue) ? selectedValue : (_tabValues$ = tabValues[0]) !== null && _tabValues$ !== void 0 ? _tabValues$ : selectedValue;
90
+ const handleValueChange = (value) => {
91
+ if (!isControlled) setUncontrolledValue(value);
92
+ onChange === null || onChange === void 0 || onChange({ value });
93
+ };
57
94
  const { tabListProps } = useTabList({
58
95
  "aria-label": ariaLabel,
59
96
  "aria-labelledby": ariaLabelledBy,
@@ -78,17 +115,31 @@ const UnderlinePanels = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabel
78
115
  const wrapperWidth = resizeObserverEntries[0].contentRect.width;
79
116
  setIconsVisible(wrapperWidth > listWidthRef.current);
80
117
  }, wrapperRef, []);
81
- !(tabs.filter((tab) => {
82
- const ariaSelected = /*#__PURE__*/ React.isValidElement(tab) && tab.props["aria-selected"];
83
- return ariaSelected === true || ariaSelected === "true";
84
- }).length <= 1) && invariant(false, "Only one tab can be selected at a time.");
85
- !(tabs.length === tabPanels.length) && invariant(false, `The number of tabs and panels must be equal. Counted ${tabs.length} tabs and ${tabPanels.length} panels.`);
118
+ {
119
+ const selectedTabs = tabs.filter((tab) => {
120
+ const ariaSelected = /*#__PURE__*/ React.isValidElement(tab) && tab.props["aria-selected"];
121
+ return ariaSelected === true || ariaSelected === "true";
122
+ });
123
+ !(selectedTabs.length <= 1) && invariant(false, "Only one tab can be selected at a time.");
124
+ !(tabs.length === tabPanels.length) && invariant(false, `The number of tabs and panels must be equal. Counted ${tabs.length} tabs and ${tabPanels.length} panels.`);
125
+ const duplicateTabValue = tabValues.find((value, index) => tabValues.indexOf(value) !== index);
126
+ !(duplicateTabValue === void 0) && invariant(false, `Every tab must have a unique \`value\`. Found duplicate "${duplicateTabValue}".`);
127
+ const duplicatePanelValue = panelValues.find((value, index) => panelValues.indexOf(value) !== index);
128
+ !(duplicatePanelValue === void 0) && invariant(false, `Every panel must have a unique \`value\`. Found duplicate "${duplicatePanelValue}".`);
129
+ const unmatchedTabValue = tabValues.find((value) => !panelValues.includes(value));
130
+ !(unmatchedTabValue === void 0) && invariant(false, `Tab with \`value\` "${unmatchedTabValue}" has no matching panel. Give a \`UnderlinePanels.Panel\` the same \`value\`.`);
131
+ warning(isControlled && selectedTabs.length > 0, "UnderlinePanels: do not combine the controlled `value` prop with `aria-selected` on a tab. `value` takes precedence; use `value`/`onChange` (or `defaultValue`) to drive selection.");
132
+ warning(controlledValue !== void 0 && defaultValue !== void 0, "UnderlinePanels: do not combine the controlled `value` prop with `defaultValue`. `value` takes precedence; use one or the other.");
133
+ const providedValue = controlledValue !== null && controlledValue !== void 0 ? controlledValue : defaultValue;
134
+ warning(providedValue !== void 0 && tabValues.length > 0 && !tabValues.includes(providedValue), `UnderlinePanels: the selected value "${providedValue}" does not match any tab, so the first tab is selected instead. Provide a \`value\`/\`defaultValue\` that matches a \`UnderlinePanels.Tab\` \`value\`.`);
135
+ }
86
136
  return /*#__PURE__*/ jsx(UnderlinePanelsContext.Provider, {
87
137
  value: contextValue,
88
138
  children: /*#__PURE__*/ jsxs(Tabs, {
89
139
  id: parentId,
90
- value: selectedValue,
91
- onValueChange: ({ value }) => setSelectedValue(value),
140
+ value: effectiveValue,
141
+ activationMode,
142
+ onValueChange: ({ value }) => handleValueChange(value),
92
143
  children: [/*#__PURE__*/ jsx(UnderlineWrapper, {
93
144
  ref: wrapperRef,
94
145
  "data-icons-visible": iconsVisible,
@@ -3394,39 +3394,39 @@
3394
3394
  "source": "https://github.com/primer/react/tree/main/packages/react/src/Flash",
3395
3395
  "id": "flash",
3396
3396
  "name": "Flash",
3397
- "status": "alpha",
3397
+ "status": "deprecated",
3398
3398
  "a11yReviewed": "2025-01-08",
3399
3399
  "stories": [
3400
3400
  {
3401
- "id": "components-flash--default",
3401
+ "id": "deprecated-components-flash--default",
3402
3402
  "code": "() => <Flash>Default</Flash>"
3403
3403
  },
3404
3404
  {
3405
- "id": "components-flash-features--success",
3405
+ "id": "deprecated-components-flash-features--success",
3406
3406
  "code": "() => (\n <Flash\n variant=\"success\"\n style={{\n display: 'grid',\n gridTemplateColumns: 'min-content 1fr minmax(0, auto)',\n gridTemplateAreas: `'visual message actions'`,\n }}\n >\n <div className={classes.Visual}>\n <CheckCircleIcon aria-label=\"Success\" />\n </div>\n <div className={classes.Message}>Success</div>\n </Flash>\n)"
3407
3407
  },
3408
3408
  {
3409
- "id": "components-flash-features--danger",
3409
+ "id": "deprecated-components-flash-features--danger",
3410
3410
  "code": "() => (\n <Flash\n variant=\"danger\"\n style={{\n display: 'grid',\n gridTemplateColumns: 'min-content 1fr minmax(0, auto)',\n gridTemplateAreas: `'visual message actions'`,\n }}\n >\n <div className={classes.Visual}>\n <InfoIcon aria-label=\"Danger\" />\n </div>\n <div className={classes.Message}>Danger</div>\n </Flash>\n)"
3411
3411
  },
3412
3412
  {
3413
- "id": "components-flash-features--warning",
3413
+ "id": "deprecated-components-flash-features--warning",
3414
3414
  "code": "() => (\n <Flash\n variant=\"warning\"\n style={{\n display: 'grid',\n gridTemplateColumns: 'min-content 1fr minmax(0, auto)',\n gridTemplateAreas: `'visual message actions'`,\n }}\n >\n <div className={classes.Visual}>\n <AlertIcon aria-label=\"Warning\" />\n </div>\n <div className={classes.Message}>Warning</div>\n </Flash>\n)"
3415
3415
  },
3416
3416
  {
3417
- "id": "components-flash-features--full",
3417
+ "id": "deprecated-components-flash-features--full",
3418
3418
  "code": "() => (\n <Flash\n full\n style={{\n display: 'grid',\n gridTemplateColumns: 'min-content 1fr minmax(0, auto)',\n gridTemplateAreas: `'visual message actions'`,\n }}\n >\n <div className={classes.Visual}>\n <InfoIcon aria-label=\"Info\" />\n </div>\n <div className={classes.Message}>Full</div>\n </Flash>\n)"
3419
3419
  },
3420
3420
  {
3421
- "id": "components-flash-features--with-icon-and-action",
3421
+ "id": "deprecated-components-flash-features--with-icon-and-action",
3422
3422
  "code": "() => (\n <Flash className={classes.WithIconAndAction}>\n <div className={classes.Visual}>\n <InfoIcon aria-label=\"Info\" />\n </div>\n <div className={classes.Message}>\n This is a flash message with an icon and an action.\n <Link href=\"/\"> Learn more.</Link>\n </div>\n <div className={classes.ActionsResponsive}>\n <Button>Join waitlist</Button>\n </div>\n </Flash>\n)"
3423
3423
  },
3424
3424
  {
3425
- "id": "components-flash-features--with-icon-action-dismiss",
3425
+ "id": "deprecated-components-flash-features--with-icon-action-dismiss",
3426
3426
  "code": "() => (\n <Flash className={classes.WithIconActionDismiss}>\n <div className={classes.Visual}>\n <InfoIcon aria-label=\"Info\" />\n </div>\n <div className={classes.Message}>\n This is a flash message with an icon and an action.\n <Link href=\"/\"> Learn more.</Link>\n </div>\n <div className={classes.ActionsResponsive}>\n <Button>Join waitlist</Button>\n </div>\n <div className={classes.Close}>\n <IconButton variant=\"invisible\" icon={XIcon} aria-label=\"Dismiss\" />\n </div>\n </Flash>\n)"
3427
3427
  }
3428
3428
  ],
3429
- "importPath": "@primer/react",
3429
+ "importPath": "@primer/react/deprecated",
3430
3430
  "props": [
3431
3431
  {
3432
3432
  "name": "full",
@@ -9605,6 +9605,22 @@
9605
9605
  {
9606
9606
  "id": "experimental-components-underlinepanels-features--with-icons-hidden-on-narrow-screen",
9607
9607
  "code": "() => (\n <UnderlinePanels aria-label=\"Tabs with icons\">\n <UnderlinePanels.Tab icon={CodeIcon}>Tab 1</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={EyeIcon}>Tab 2</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={GitPullRequestIcon}>Tab 3</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={CommentDiscussionIcon}>\n Tab 4\n </UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={PlayIcon}>Tab 5</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={ProjectIcon}>Tab 6</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={GraphIcon}>Tab 7</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={GearIcon}>Tab 8</UnderlinePanels.Tab>\n <UnderlinePanels.Tab icon={ShieldLockIcon}>Tab 9</UnderlinePanels.Tab>\n <UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 3</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 4</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 5</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 6</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 7</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 8</UnderlinePanels.Panel>\n <UnderlinePanels.Panel>Panel 9</UnderlinePanels.Panel>\n </UnderlinePanels>\n)"
9608
+ },
9609
+ {
9610
+ "id": "experimental-components-underlinepanels-features--controlled",
9611
+ "code": "() => {\n const [refType, setRefType] = useState('branch')\n return (\n <>\n <FlagState />\n <UnderlinePanels\n aria-label=\"Ref type\"\n value={refType}\n onChange={({ value }) => {\n action('onChange')({\n value,\n })\n setRefType(value)\n }}\n >\n <UnderlinePanels.Tab value=\"branch\" icon={GitBranchIcon}>\n Branches\n </UnderlinePanels.Tab>\n <UnderlinePanels.Tab value=\"tag\" icon={TagIcon}>\n Tags\n </UnderlinePanels.Tab>\n <UnderlinePanels.Panel value=\"branch\">\n Find or create a branch…\n </UnderlinePanels.Panel>\n <UnderlinePanels.Panel value=\"tag\">\n Search or create a new tag…\n </UnderlinePanels.Panel>\n </UnderlinePanels>\n <p>\n Selected ref type: <strong>{refType}</strong>\n </p>\n </>\n )\n}"
9612
+ },
9613
+ {
9614
+ "id": "experimental-components-underlinepanels-features--uncontrolled",
9615
+ "code": "() => (\n <>\n <FlagState />\n <UnderlinePanels\n aria-label=\"Ref type\"\n defaultValue=\"tag\"\n onChange={({ value }) => {\n action('onChange')({\n value,\n })\n }}\n >\n <UnderlinePanels.Tab value=\"branch\">Branches</UnderlinePanels.Tab>\n <UnderlinePanels.Tab value=\"tag\">Tags</UnderlinePanels.Tab>\n <UnderlinePanels.Panel value=\"branch\">\n Find or create a branch…\n </UnderlinePanels.Panel>\n <UnderlinePanels.Panel value=\"tag\">\n Search or create a new tag…\n </UnderlinePanels.Panel>\n </UnderlinePanels>\n </>\n)"
9616
+ },
9617
+ {
9618
+ "id": "experimental-components-underlinepanels-features--manual-activation",
9619
+ "code": "() => {\n const [refType, setRefType] = useState('branch')\n return (\n <>\n <FlagState />\n <p>\n With <code>activationMode=&quot;manual&quot;</code>, arrow keys only\n move focus; press Enter or Space (or click) to commit selection. Prefer\n this when switching tabs triggers async work like a fetch.\n </p>\n <UnderlinePanels\n aria-label=\"Ref type\"\n value={refType}\n activationMode=\"manual\"\n onChange={({ value }) => {\n action('onChange')({\n value,\n })\n setRefType(value)\n }}\n >\n <UnderlinePanels.Tab value=\"branch\">Branches</UnderlinePanels.Tab>\n <UnderlinePanels.Tab value=\"tag\">Tags</UnderlinePanels.Tab>\n <UnderlinePanels.Panel value=\"branch\">\n Find or create a branch…\n </UnderlinePanels.Panel>\n <UnderlinePanels.Panel value=\"tag\">\n Search or create a new tag…\n </UnderlinePanels.Panel>\n </UnderlinePanels>\n </>\n )\n}"
9620
+ },
9621
+ {
9622
+ "id": "experimental-components-underlinepanels-features--in-overlay",
9623
+ "code": "() => {\n const [open, setOpen] = useState(false)\n const [refType, setRefType] = useState('branch')\n return (\n <>\n <FlagState />\n <AnchoredOverlay\n open={open}\n onOpen={() => setOpen(true)}\n onClose={() => setOpen(false)}\n renderAnchor={(props) => <Button {...props}>Select ref type</Button>}\n overlayProps={{\n role: 'dialog',\n 'aria-modal': true,\n 'aria-label': 'Select a ref type',\n style: {\n width: '320px',\n },\n }}\n focusZoneSettings={{\n disabled: true,\n }}\n >\n <UnderlinePanels\n aria-label=\"Ref type\"\n value={refType}\n onChange={({ value }) => {\n action('onChange')({\n value,\n })\n setRefType(value)\n }}\n >\n <UnderlinePanels.Tab value=\"branch\">Branches</UnderlinePanels.Tab>\n <UnderlinePanels.Tab value=\"tag\">Tags</UnderlinePanels.Tab>\n <UnderlinePanels.Panel value=\"branch\">\n Find or create a branch…\n </UnderlinePanels.Panel>\n <UnderlinePanels.Panel value=\"tag\">\n Search or create a new tag…\n </UnderlinePanels.Panel>\n </UnderlinePanels>\n </AnchoredOverlay>\n </>\n )\n}"
9608
9624
  }
9609
9625
  ],
9610
9626
  "importPath": "@primer/react/experimental",
@@ -9621,6 +9637,30 @@
9621
9637
  "defaultValue": "",
9622
9638
  "description": "ID of the element containing the name for the tab list"
9623
9639
  },
9640
+ {
9641
+ "name": "value",
9642
+ "type": "string",
9643
+ "defaultValue": "",
9644
+ "description": "The value of the selected tab, keyed to each `UnderlinePanels.Tab`/`UnderlinePanels.Panel` `value`. Provide this (with `onChange`) for a controlled component where the selected tab is the single source of truth. Requires the `primer_react_underline_panels_controlled` feature flag."
9645
+ },
9646
+ {
9647
+ "name": "defaultValue",
9648
+ "type": "string",
9649
+ "defaultValue": "",
9650
+ "description": "The value of the tab selected by default, for an uncontrolled component. Cannot be combined with `value`."
9651
+ },
9652
+ {
9653
+ "name": "onChange",
9654
+ "type": "({value}: {value: string}) => void",
9655
+ "defaultValue": "",
9656
+ "description": "Callback fired whenever the selected tab changes, for every selection method — pointer, Enter/Space, and Arrow/Home/End keys. Unlike `UnderlinePanels.Tab`'s `onSelect` (which only fires on click and Enter/Space), this makes the selected value usable as a single source of truth for data-driven tabs."
9657
+ },
9658
+ {
9659
+ "name": "activationMode",
9660
+ "type": "'automatic' | 'manual'",
9661
+ "defaultValue": "'automatic'",
9662
+ "description": "Controls how tabs are activated with the keyboard. `'automatic'` selects on focus (Arrow/Home/End select immediately); `'manual'` moves focus only and commits selection on Enter, Space, or click. Prefer `'manual'` when displaying a panel is not instant (e.g. it triggers a network request). Requires the `primer_react_underline_panels_controlled` feature flag."
9663
+ },
9624
9664
  {
9625
9665
  "name": "children",
9626
9666
  "type": "Array<UnderlinePanels.Tab | UnderlinePanels.Panel>",
@@ -9651,6 +9691,12 @@
9651
9691
  {
9652
9692
  "name": "UnderlinePanels.Tab",
9653
9693
  "props": [
9694
+ {
9695
+ "name": "value",
9696
+ "type": "string",
9697
+ "defaultValue": "",
9698
+ "description": "A value that uniquely identifies this tab, paired with the `UnderlinePanels.Panel` of the same `value`. Provide this to use domain values (e.g. `'branch'`) with the container's controlled `value`/`onChange` API. When omitted, tabs and panels are paired by DOM order."
9699
+ },
9654
9700
  {
9655
9701
  "name": "aria-selected",
9656
9702
  "type": "| boolean | 'true' | 'false'",
@@ -9661,7 +9707,7 @@
9661
9707
  "name": "onSelect",
9662
9708
  "type": "(event) => void",
9663
9709
  "defaultValue": "",
9664
- "description": "The handler that gets called when the tab is selected"
9710
+ "description": "The handler that gets called when the tab is selected. Note: this fires only on click and Enter/Space — not on arrow-key navigation. For a callback that fires for every selection method, use the container's `onChange` prop instead."
9665
9711
  },
9666
9712
  {
9667
9713
  "name": "counter",
@@ -9679,7 +9725,14 @@
9679
9725
  },
9680
9726
  {
9681
9727
  "name": "UnderlinePanels.Panel",
9682
- "props": [],
9728
+ "props": [
9729
+ {
9730
+ "name": "value",
9731
+ "type": "string",
9732
+ "defaultValue": "",
9733
+ "description": "A value that uniquely identifies this panel, paired with the `UnderlinePanels.Tab` of the same `value`. When omitted, tabs and panels are paired by DOM order."
9734
+ }
9735
+ ],
9683
9736
  "passthrough": {
9684
9737
  "element": "div",
9685
9738
  "url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#Attributes"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@primer/react",
3
3
  "type": "module",
4
- "version": "38.37.0-rc.0b6dd7e38",
4
+ "version": "38.37.0-rc.1789e76c6",
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",