Skip to content

Fix/nextjs hydration errors 12250#12251

Open
arpanroy41 wants to merge 12 commits intopatternfly:mainfrom
arpanroy41:fix/nextjs-hydration-errors-12250
Open

Fix/nextjs hydration errors 12250#12251
arpanroy41 wants to merge 12 commits intopatternfly:mainfrom
arpanroy41:fix/nextjs-hydration-errors-12250

Conversation

@arpanroy41
Copy link
Contributor

@arpanroy41 arpanroy41 commented Feb 25, 2026

Replaced the counter-based ID generation with React's useId() hook, which produces deterministic IDs that are consistent between server and client rendering.

New utilities introduced:

  • useSSRSafeId(prefix?) — A hook that wraps React.useId() (React 18+) with an optional prefix. Falls back to a useState-based counter for React 17 compatibility.

  • SSRSafeIds — A render-prop component that exposes useSSRSafeId to class-component patterns, generating both a component ID and an optional OUIA ID.

Refactored utilities:

  • GenerateId — Now delegates to useSSRSafeId internally. API unchanged; isRandom prop is deprecated (no-op).

  • useOUIAId / useOUIAProps — Now use useSSRSafeId for generated OUIA IDs instead of the global counter.

ID format change
Generated IDs change from sequential integers to React useId() tokens:

Before: pf-random-id-0, OUIA-Generated-Tabs-3
After: pf-random-id-:r0:, OUIA-Generated-Tabs-:r1:

This is a non-breaking change — these IDs are auto-generated internals, not user-facing API. Any consumer passing an explicit id prop is unaffected.

Summary by CodeRabbit

  • Bug Fixes

    • Improved SSR and Strict Mode compatibility for component IDs, reducing hydration and accessibility issues.
    • Stabilized accessible labeling to ensure consistent aria attributes across environments.
  • Refactor

    • Unified ID/OUIA generation across the library for more predictable behavior.
  • Tests

    • Relaxed fragile ID-based assertions to accommodate stable ID generation changes.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • packages/react-core/src/components/FormSelect/__tests__/__snapshots__/FormSelect.test.tsx.snap is excluded by !**/*.snap

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 268aa4a1-5cdc-4bc6-ac53-c8272c0f651b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Refactors ID and OUIA generation across the codebase to SSR-safe patterns by adding useSSRSafeId and SSRSafeIds, converting/removing legacy ID utilities/wrappers, and updating ~50 components and tests to use the new helpers without changing public APIs.

Changes

Cohort / File(s) Summary
New helpers & exports
packages/react-core/src/helpers/useSSRSafeId.ts, packages/react-core/src/helpers/SSRSafeIds/SSRSafeIds.tsx, packages/react-core/src/helpers/index.ts
Add useSSRSafeId hook and SSRSafeIds render-prop component; re-export from helpers index.
GenerateId / OUIA core
packages/react-core/src/helpers/GenerateId/GenerateId.ts, packages/react-core/src/helpers/OUIA/ouia.ts
Convert GenerateId to functional hook-based impl; refactor OUIA ID generation to use useSSRSafeId and simplify default OUIA id logic.
Class components → SSRSafeIds
packages/react-core/src/components/.../Card/CardHeader.tsx, .../Checkbox/Checkbox.tsx, .../FormSelect/FormSelect.tsx, .../Menu/Menu.tsx, .../MenuToggle/*, .../Modal/Modal.tsx, .../Radio/Radio.tsx, .../Switch/Switch.tsx, .../Tabs/Tabs.tsx, .../TextInput/TextInput.tsx, .../Toolbar/Toolbar.tsx, .../deprecated/*/Modal.tsx, .../deprecated/*/Chip.tsx
Wrap class-component render paths with SSRSafeIds (or equivalent) to provide stable IDs/OUIA ids; remove state-based ID tracking and adjust class signatures and render flows.
Functional components → useSSRSafeId
packages/react-core/src/components/.../CalendarMonth.tsx, .../DualListSelector/*, .../JumpLinks/JumpLinks.tsx, .../Popover/Popover.tsx, .../Nav/NavGroup.tsx, .../SearchInput/AdvancedSearchMenu.tsx, .../Tooltip/Tooltip.tsx, .../Popover/Popover.tsx, .../Select/Select.tsx
Replace ad-hoc/getUniqueId defaults with useSSRSafeId; compute id as prop
GenerateId wrapper removals / render simplifications
packages/react-core/src/components/.../AboutModal/AboutModal.tsx, .../DataList/DataListCheck.tsx, .../Drawer/DrawerPanelContent.tsx, .../ExpandableSection/ExpandableSection.tsx, .../Form/*, .../MultipleFileUpload/*, .../TimePicker/TimePicker.tsx, .../TreeView/TreeViewListItem.tsx, .../Progress/Progress.tsx
Remove nested GenerateId render-prop usage or replace with useSSRSafeId/inline GenerateId usage; consolidate id resolution and simplify JSX structure.
Nav / Toolbar / Menu specific updates
packages/react-core/src/components/Nav/Nav.tsx, .../Nav/NavExpandable.tsx, packages/react-core/src/components/Menu/Menu.tsx, packages/react-core/src/components/Menu/MenuItem.tsx, packages/react-core/src/components/Toolbar/*
Migrate OUIA/id handling to SSRSafeIds or useSSRSafeId; remove getDefaultOUIAId/state fields; add minor lifecycle/state adjustments where needed (e.g., toolbar viewport tracking).
Tests & integration updates
packages/react-core/src/components/JumpLinks/__tests__/JumpLinks.test.tsx, packages/react-core/src/components/TreeView/__tests__/TreeViewListItem.test.tsx, packages/react-integration/cypress/integration/switch.spec.ts
Relax/move ID-related assertions (no mocked getUniqueId), update expectations to check presence or looser patterns, and adjust referenced test IDs.
Deprecated components updates
packages/react-core/src/deprecated/components/DualListSelector/*, packages/react-core/src/deprecated/components/Modal/Modal.tsx, packages/react-core/src/deprecated/components/Chip/Chip.tsx
Same ID/OUIA migration to useSSRSafeId / SSRSafeIds; maintain public APIs while changing internal id derivation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • nicolethoen
  • thatblindgeye
  • mcoker
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/nextjs hydration errors 12250' directly relates to the main objective of this PR, which is fixing Next.js hydration errors through deterministic ID generation using React.useId().
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

@patternfly-build
Copy link
Collaborator

patternfly-build commented Feb 25, 2026

arpanroy41 and others added 3 commits February 25, 2026 22:12
…ultiple components

- Updated AboutModal, CalendarMonth, CardHeader, Checkbox, DataListCheck, DrawerPanelContent, DualListSelector, DualListSelectorListItem, DualListSelectorListWrapper, DualListSelectorPane, ExpandableSection, FormGroup, InternalFormFieldGroup, JumpLinks, and MenuItem components to use useSSRSafeId for generating unique IDs instead of GenerateId.
- This change improves consistency and simplifies ID generation across components.
…omponents

- Updated Checkbox, ExpandableSection, FormSelect, Menu, MenuToggle, MenuToggleCheckbox, Modal, Nav, NavExpandable, and Progress components to utilize SSRSafeIds for generating unique IDs.
- This change enhances ID management and ensures consistency across components.
The deprecated Table class component in react-table still requires
getDefaultOUIAId which cannot be replaced with hooks. Re-added as
a simplified legacy function marked @deprecated.

Co-authored-by: Cursor <cursoragent@cursor.com>
@arpanroy41 arpanroy41 force-pushed the fix/nextjs-hydration-errors-12250 branch from b69f6fb to b39dbde Compare February 25, 2026 16:46
- improve interface definitions by removing unnecessary line breaks and enhancing readability.
- This change maintains consistency in code style across the codebase.
Reformatted with Prettier 3.8.1 to match CI and updated snapshot
- Updated snapshots for CodeEditor, AboutModalBoxCloseButton, AlertActionCloseButton, AlertIcon, ClipboardCopyButton, and others to reflect recent changes in SVG icons and structure.
- Ensured consistency in icon rendering across components by replacing placeholder paths with actual SVG elements.
- Replaced the import of React's useId with a direct import from 'react' in the useSSRSafeId helper.
- This change simplifies the code and ensures compatibility with the latest React features.
…sign

- Added a resize event listener to update the viewport width in the ToolbarLabelGroup component.
- Ensured proper cleanup of the event listener in componentWillUnmount.
- Updated Cypress tests for the Switch component to reflect changes in element visibility and interaction.
@arpanroy41 arpanroy41 marked this pull request as ready for review February 26, 2026 11:43
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
packages/react-core/src/components/Toolbar/ToolbarLabelGroupContent.tsx (1)

46-48: Avoid redundant setState during resize storms.

Line 47 sets state on every resize event, even when the value is unchanged. A quick equality guard reduces unnecessary rerenders.

♻️ Proposed refactor
  handleResize = () => {
-    this.setState({ viewportWidth: window.innerWidth });
+    const nextViewportWidth = window.innerWidth;
+    if (nextViewportWidth !== this.state.viewportWidth) {
+      this.setState({ viewportWidth: nextViewportWidth });
+    }
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Toolbar/ToolbarLabelGroupContent.tsx`
around lines 46 - 48, The resize handler handleResize currently calls
this.setState({ viewportWidth: window.innerWidth }) on every resize event
causing redundant renders; update handleResize to first compare
window.innerWidth to this.state.viewportWidth and only call this.setState when
the value differs (i.e., guard against equality) to prevent unnecessary
rerenders during resize storms.
packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx (1)

50-51: Missing trailing hyphen in prefix for consistency.

The prefix 'dual-list-selector-list-item' is missing a trailing hyphen compared to similar components in this PR (e.g., the deprecated version uses 'dual-list-selector-list-item-'). This results in generated IDs like dual-list-selector-list-item:r0: instead of dual-list-selector-list-item-:r0:.

💡 Suggested fix for consistency
-  const generatedId = useSSRSafeId('dual-list-selector-list-item');
+  const generatedId = useSSRSafeId('dual-list-selector-list-item-');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx`
around lines 50 - 51, The generated ID prefix used by useSSRSafeId in
DualListSelectorListItem is missing a trailing hyphen; update the prefix string
passed to useSSRSafeId (currently 'dual-list-selector-list-item') to include the
trailing hyphen (e.g., 'dual-list-selector-list-item-') so that generatedId (and
the resolved id variable which falls back to generatedId via idProp ??
generatedId) matches the other components' ID format.
packages/react-core/src/components/Popover/Popover.tsx (1)

279-280: Consider using a descriptive prefix for easier debugging.

The useSSRSafeId() call uses the default 'pf-' prefix. Other components in this PR use more descriptive prefixes (e.g., 'dual-list-selector-pane-', 'pf-progress-'). A prefix like 'pf-popover-' would make generated IDs more identifiable during debugging.

💡 Suggested change
-  const generatedId = useSSRSafeId();
+  const generatedId = useSSRSafeId('pf-popover-');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Popover/Popover.tsx` around lines 279 -
280, The generated ID uses useSSRSafeId() with the default 'pf-' prefix; update
the call in Popover (where generatedId and uniqueId are defined) to pass a
descriptive prefix such as 'pf-popover-' so generatedId (and therefore uniqueId
when id is not provided) is easily identifiable during debugging; adjust the
useSSRSafeId invocation in the Popover component to include that prefix string.
packages/react-core/src/components/JumpLinks/__tests__/JumpLinks.test.tsx (1)

140-142: Strengthen the new assertions to validate ID linkage, not just truthiness.

A truthy aria-labelledby can still point to a missing element. Consider asserting the referenced node exists.

💡 Suggested test hardening
   const navigation = screen.getByRole('navigation', { name: /Toggle jump links/i });
   expect(navigation).toHaveAttribute('aria-labelledby');
-  expect(navigation.getAttribute('aria-labelledby')).toBeTruthy();
+  const labelledById = navigation.getAttribute('aria-labelledby');
+  expect(labelledById).toBeTruthy();
+  expect(document.getElementById(labelledById!)).toBeInTheDocument();
   const navigation = screen.getByRole('navigation', { name: /Jump to section/i });
   expect(navigation).toHaveAttribute('aria-labelledby');
-  expect(navigation.getAttribute('aria-labelledby')).toBeTruthy();
+  const labelledById = navigation.getAttribute('aria-labelledby');
+  expect(labelledById).toBeTruthy();
+  expect(document.getElementById(labelledById!)).toBeInTheDocument();

Also applies to: 151-153

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/JumpLinks/__tests__/JumpLinks.test.tsx`
around lines 140 - 142, Replace the loose truthiness checks for aria-labelledby
with assertions that the ID actually resolves to an existing element: read the
attribute from the navigation element
(navigation.getAttribute('aria-labelledby') -> id), assert id is a non-empty
string, then assert document.getElementById(id) is not null (and optionally that
it equals the expected label node or has expected text). Apply the same stronger
assertion pattern to the other occurrence in the same test (the block around the
second expect at 151-153).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-core/src/components/Card/CardHeader.tsx`:
- Around line 170-176: The clickable link branch in CardHeader.tsx (inside the
isClickableLinkCard handling that returns baseProps with selectableActions)
doesn't set rel for external links; update the object returned where
selectableActions.isExternalLink is used to also include rel: 'noopener
noreferrer' when target: '_blank' is set (i.e., when
selectableActions.isExternalLink is true) so external links are hardened against
opener access while keeping existing spreads like baseProps and aria/tabIndex
behavior.

In `@packages/react-core/src/components/Checkbox/Checkbox.tsx`:
- Around line 120-159: The input may receive checked={null} because
checkedProps.checked is normalized after inputRendered is created; move the
normalization so checkedProps.checked is set (e.g., checkedProps.checked =
checkedProps.checked === null ? false : checkedProps.checked) before building
inputRendered (the JSX that uses checkedProps), ensuring the ref indeterminate
logic that reads isChecked still works; update places referencing checkedProps,
inputRendered, isChecked, and WrapperComponent accordingly so the input never
receives null for the checked prop.

In
`@packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx`:
- Around line 68-72: The inner ternary is redundant: in DualListSelectorPane
remove the conditional expression "{searchInput ? searchInput : <SearchInput
isDisabled={isDisabled} />}" and simply render "searchInput" inside the div with
className={css(styles.dualListSelectorToolsFilter)} (or, if you intended to show
the default SearchInput when searchInput is falsy, change the outer condition to
always render the div and use the ternary there). Update references to
searchInput and SearchInput accordingly and keep isDisabled passed to the
default SearchInput if you choose the always-render approach.

In `@packages/react-core/src/components/FormSelect/FormSelect.tsx`:
- Around line 76-77: The generated OUIA id for FormSelect is changing because
ouiaComponentType passed into SSRSafeIds includes the mutable validated value;
update the usage in FormSelect so SSRSafeIds receives a stable ouiaComponentType
(e.g., "FormSelect" only) instead of `FormSelect${validated ? `-${validated}` :
''}` to preserve generatedOuiaId across validation changes, and if you need to
expose validation in OUIA, attach validated to a separate stable prop or data
attribute rather than altering ouiaComponentType.

In `@packages/react-core/src/components/Switch/Switch.tsx`:
- Around line 126-130: The Switch component currently renders <CheckIcon />
unconditionally in the unlabeled branch, ignoring the hasCheckIcon prop; update
the unlabeled rendering path inside the Switch component to only render the
CheckIcon when hasCheckIcon is true (same condition used in the labeled branch),
e.g. gate the element rendered inside the node with className
css(styles.switchToggleIcon) on props.hasCheckIcon so that switchToggle and
switchToggleIcon markup remains but the icon is omitted when hasCheckIcon is
false.

In `@packages/react-core/src/components/Tooltip/Tooltip.tsx`:
- Around line 166-167: The cleanup that removes the tooltip id from trigger
element aria attributes fails for useId-style ids (e.g., pf-tooltip-:r0:)
because it uses a \b-based regex; update the cleanup in the Tooltip component
(the effect that uses generatedId / id to set/remove
aria-describedby/aria-labelledby) to remove the id token safely by either
escaping the id for a RegExp (use an escape function for special chars) or,
better, by splitting the attribute value on whitespace and filtering out exact
matches of id (then rejoin and remove the attribute if empty); ensure this logic
runs both for aria-describedby and aria-labelledby and references the same id
variable (id or generatedId) used when adding the attribute.

In `@packages/react-core/src/helpers/useSSRSafeId.ts`:
- Around line 15-17: The counter-based fallback in useSSRSafeId can cause SSR
hydration mismatches because the module-level counter resets between server and
client; update useSSRSafeId to detect when the fallback path is taken (e.g.,
when useId is unavailable) and when running on the server (typeof window ===
'undefined') and emit a dev-only warning (process.env.NODE_ENV !== 'production')
that documents the limitation and advises using React 18+ or explicit IDs; also
add a short note to the component library docs/README referencing useSSRSafeId
and the SSR limitation so consumers are aware.

---

Nitpick comments:
In
`@packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx`:
- Around line 50-51: The generated ID prefix used by useSSRSafeId in
DualListSelectorListItem is missing a trailing hyphen; update the prefix string
passed to useSSRSafeId (currently 'dual-list-selector-list-item') to include the
trailing hyphen (e.g., 'dual-list-selector-list-item-') so that generatedId (and
the resolved id variable which falls back to generatedId via idProp ??
generatedId) matches the other components' ID format.

In `@packages/react-core/src/components/JumpLinks/__tests__/JumpLinks.test.tsx`:
- Around line 140-142: Replace the loose truthiness checks for aria-labelledby
with assertions that the ID actually resolves to an existing element: read the
attribute from the navigation element
(navigation.getAttribute('aria-labelledby') -> id), assert id is a non-empty
string, then assert document.getElementById(id) is not null (and optionally that
it equals the expected label node or has expected text). Apply the same stronger
assertion pattern to the other occurrence in the same test (the block around the
second expect at 151-153).

In `@packages/react-core/src/components/Popover/Popover.tsx`:
- Around line 279-280: The generated ID uses useSSRSafeId() with the default
'pf-' prefix; update the call in Popover (where generatedId and uniqueId are
defined) to pass a descriptive prefix such as 'pf-popover-' so generatedId (and
therefore uniqueId when id is not provided) is easily identifiable during
debugging; adjust the useSSRSafeId invocation in the Popover component to
include that prefix string.

In `@packages/react-core/src/components/Toolbar/ToolbarLabelGroupContent.tsx`:
- Around line 46-48: The resize handler handleResize currently calls
this.setState({ viewportWidth: window.innerWidth }) on every resize event
causing redundant renders; update handleResize to first compare
window.innerWidth to this.state.viewportWidth and only call this.setState when
the value differs (i.e., guard against equality) to prevent unnecessary
rerenders during resize storms.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbf8cae and 9b41afd.

⛔ Files ignored due to path filters (78)
  • packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap is excluded by !**/*.snap
  • packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditorControl.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxCloseButton.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxHeader.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionCloseButton.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionLink.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Card/__tests__/__snapshots__/Card.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Card/__tests__/__snapshots__/CardHeader.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopy.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyButton.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyToggle.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavHome.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavSearch.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListCheck.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListToggle.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/DataList/__tests__/__snapshots__/DataListToggle.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Drawer/__tests__/Generated/__snapshots__/DrawerPanelContent.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Drawer/__tests__/__snapshots__/Drawer.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/EmptyState/__tests__/__snapshots__/EmptyState.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUpload.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadField.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Form/__tests__/__snapshots__/FormFieldGroup.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/FormSelect/__tests__/__snapshots__/FormSelect.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/JumpLinks/__tests__/__snapshots__/JumpLinks.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Label/__tests__/__snapshots__/Label.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Label/__tests__/__snapshots__/LabelGroup.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/LoginPage/__tests__/__snapshots__/LoginForm.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/LoginPage/__tests__/__snapshots__/LoginMainHeader.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/LoginPage/__tests__/__snapshots__/LoginPage.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Menu/__tests__/__snapshots__/Menu.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/MenuToggle/__tests__/__snapshots__/MenuToggle.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/MultipleFileUpload/__tests__/__snapshots__/MultipleFileUploadButton.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/MultipleFileUpload/__tests__/__snapshots__/MultipleFileUploadMain.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/MultipleFileUpload/__tests__/__snapshots__/MultipleFileUploadStatus.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/MultipleFileUpload/__tests__/__snapshots__/MultipleFileUploadStatusItem.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Nav/__tests__/Generated/__snapshots__/NavExpandable.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Nav/__tests__/Generated/__snapshots__/NavItem.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Nav/__tests__/__snapshots__/Nav.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/NotificationBadge/__tests__/__snapshots__/NotificationBadge.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/NumberInput/__tests__/__snapshots__/NumberInput.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/OverflowMenu/__tests__/Generated/__snapshots__/OverflowMenuDropdownItem.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Page/__tests__/__snapshots__/Page.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Pagination/__tests__/Generated/__snapshots__/PaginationNavigation.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Pagination/__tests__/Generated/__snapshots__/PaginationOptionsMenu.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Popover/__tests__/Generated/__snapshots__/PopoverCloseButton.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/SearchInput/__tests__/__snapshots__/SearchInput.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/SkipToContent/__tests__/__snapshots__/SkipToContent.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Slider/__tests__/__snapshots__/Slider.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Switch/__tests__/__snapshots__/Switch.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tab.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/TextInput/__tests__/__snapshots__/TextInput.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/deprecated/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/deprecated/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/deprecated/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/deprecated/components/Wizard/__tests__/Generated/__snapshots__/WizardFooterInternal.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/deprecated/components/Wizard/__tests__/Generated/__snapshots__/WizardNav.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/deprecated/components/Wizard/__tests__/Generated/__snapshots__/WizardNavItem.test.tsx.snap is excluded by !**/*.snap, !**/generated/**
  • packages/react-core/src/deprecated/components/Wizard/__tests__/__snapshots__/Wizard.test.tsx.snap is excluded by !**/*.snap
  • packages/react-core/src/helpers/GenerateId/__tests__/__snapshots__/GenerateId.test.tsx.snap is excluded by !**/*.snap
  • packages/react-drag-drop/src/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap is excluded by !**/*.snap
  • packages/react-table/src/components/Table/__tests__/__snapshots__/RowWrapper.test.tsx.snap is excluded by !**/*.snap
  • packages/react-table/src/components/Table/__tests__/__snapshots__/Table.test.tsx.snap is excluded by !**/*.snap
  • packages/react-table/src/components/Table/__tests__/__snapshots__/Tr.test.tsx.snap is excluded by !**/*.snap
  • packages/react-table/src/deprecated/components/Table/__tests__/__snapshots__/Table.test.tsx.snap is excluded by !**/*.snap
  • packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/SimpleDropdown.test.tsx.snap is excluded by !**/*.snap
  • packages/react-templates/src/components/Select/__tests__/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap is excluded by !**/*.snap
  • packages/react-templates/src/components/Select/__tests__/__snapshots__/MultiTypeaheadSelect.test.tsx.snap is excluded by !**/*.snap
  • packages/react-templates/src/components/Select/__tests__/__snapshots__/SimpleSelect.test.tsx.snap is excluded by !**/*.snap
  • packages/react-templates/src/components/Select/__tests__/__snapshots__/TypeaheadSelect.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (51)
  • packages/react-core/src/components/AboutModal/AboutModal.tsx
  • packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx
  • packages/react-core/src/components/Card/CardHeader.tsx
  • packages/react-core/src/components/Checkbox/Checkbox.tsx
  • packages/react-core/src/components/DataList/DataListCheck.tsx
  • packages/react-core/src/components/Drawer/DrawerPanelContent.tsx
  • packages/react-core/src/components/DualListSelector/DualListSelector.tsx
  • packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx
  • packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx
  • packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx
  • packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx
  • packages/react-core/src/components/Form/FormGroup.tsx
  • packages/react-core/src/components/Form/InternalFormFieldGroup.tsx
  • packages/react-core/src/components/FormSelect/FormSelect.tsx
  • packages/react-core/src/components/JumpLinks/JumpLinks.tsx
  • packages/react-core/src/components/JumpLinks/__tests__/JumpLinks.test.tsx
  • packages/react-core/src/components/Menu/Menu.tsx
  • packages/react-core/src/components/Menu/MenuItem.tsx
  • packages/react-core/src/components/MenuToggle/MenuToggle.tsx
  • packages/react-core/src/components/MenuToggle/MenuToggleCheckbox.tsx
  • packages/react-core/src/components/Modal/Modal.tsx
  • packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx
  • packages/react-core/src/components/Nav/Nav.tsx
  • packages/react-core/src/components/Nav/NavExpandable.tsx
  • packages/react-core/src/components/Nav/NavGroup.tsx
  • packages/react-core/src/components/Popover/Popover.tsx
  • packages/react-core/src/components/Progress/Progress.tsx
  • packages/react-core/src/components/Radio/Radio.tsx
  • packages/react-core/src/components/SearchInput/AdvancedSearchMenu.tsx
  • packages/react-core/src/components/Select/Select.tsx
  • packages/react-core/src/components/Switch/Switch.tsx
  • packages/react-core/src/components/Tabs/Tabs.tsx
  • packages/react-core/src/components/TextInput/TextInput.tsx
  • packages/react-core/src/components/TimePicker/TimePicker.tsx
  • packages/react-core/src/components/Toolbar/Toolbar.tsx
  • packages/react-core/src/components/Toolbar/ToolbarLabelGroupContent.tsx
  • packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx
  • packages/react-core/src/components/Tooltip/Tooltip.tsx
  • packages/react-core/src/components/TreeView/TreeViewListItem.tsx
  • packages/react-core/src/components/TreeView/__tests__/TreeViewListItem.test.tsx
  • packages/react-core/src/deprecated/components/Chip/Chip.tsx
  • packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListItem.tsx
  • packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListWrapper.tsx
  • packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorPane.tsx
  • packages/react-core/src/deprecated/components/Modal/Modal.tsx
  • packages/react-core/src/helpers/GenerateId/GenerateId.ts
  • packages/react-core/src/helpers/OUIA/ouia.ts
  • packages/react-core/src/helpers/SSRSafeIds/SSRSafeIds.tsx
  • packages/react-core/src/helpers/index.ts
  • packages/react-core/src/helpers/useSSRSafeId.ts
  • packages/react-integration/cypress/integration/switch.spec.ts

Comment on lines +170 to 176
if (isClickableLinkCard) {
return {
...baseProps,
href: selectableActions.to,
...(isCardDisabled && { tabIndex: -1, 'aria-disabled': true }),
...(selectableActions.isExternalLink && { target: '_blank' })
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden external-link clickable actions with rel when using _blank.

Opening links in a new tab without rel="noopener noreferrer" leaves the opener vulnerable.

🔐 Proposed fix
           if (isClickableLinkCard) {
             return {
               ...baseProps,
               href: selectableActions.to,
               ...(isCardDisabled && { tabIndex: -1, 'aria-disabled': true }),
-              ...(selectableActions.isExternalLink && { target: '_blank' })
+              ...(selectableActions.isExternalLink && {
+                target: '_blank',
+                rel: selectableActions.selectableActionProps?.rel ?? 'noopener noreferrer'
+              })
             };
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isClickableLinkCard) {
return {
...baseProps,
href: selectableActions.to,
...(isCardDisabled && { tabIndex: -1, 'aria-disabled': true }),
...(selectableActions.isExternalLink && { target: '_blank' })
};
if (isClickableLinkCard) {
return {
...baseProps,
href: selectableActions.to,
...(isCardDisabled && { tabIndex: -1, 'aria-disabled': true }),
...(selectableActions.isExternalLink && {
target: '_blank',
rel: selectableActions.selectableActionProps?.rel ?? 'noopener noreferrer'
})
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Card/CardHeader.tsx` around lines 170 -
176, The clickable link branch in CardHeader.tsx (inside the isClickableLinkCard
handling that returns baseProps with selectableActions) doesn't set rel for
external links; update the object returned where
selectableActions.isExternalLink is used to also include rel: 'noopener
noreferrer' when target: '_blank' is set (i.e., when
selectableActions.isExternalLink is true) so external links are hardened against
opener access while keeping existing spreads like baseProps and aria/tabIndex
behavior.

Comment on lines +120 to +159
const inputRendered = (
<input
{...props}
className={css(styles.checkInput, inputClassName)}
type="checkbox"
onChange={this.handleChange}
aria-invalid={!isValid}
aria-label={ariaLabel}
aria-describedby={ariaDescribedByValue}
disabled={isDisabled}
required={isRequired}
ref={(elem) => {
elem && (elem.indeterminate = isChecked === null);
}}
{...checkedProps}
{...getOUIAProps(Checkbox.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId, ouiaSafe)}
/>
);

const wrapWithLabel = (isLabelWrapped && !component) || component === 'label';
const wrapWithLabel = (isLabelWrapped && !component) || component === 'label';

const Label = wrapWithLabel ? 'span' : 'label';
const labelRendered = label ? (
<Label
className={css(styles.checkLabel, isDisabled && styles.modifiers.disabled)}
htmlFor={!wrapWithLabel ? props.id : undefined}
>
{label}
{isRequired && (
<span className={css(styles.checkLabelRequired)} aria-hidden="true">
{ASTERISK}
</span>
)}
</Label>
) : null;
const Label = wrapWithLabel ? 'span' : 'label';
const labelRendered = label ? (
<Label
className={css(styles.checkLabel, isDisabled && styles.modifiers.disabled)}
htmlFor={!wrapWithLabel ? props.id : undefined}
>
{label}
{isRequired && (
<span className={css(styles.checkLabelRequired)} aria-hidden="true">
{ASTERISK}
</span>
)}
</Label>
) : null;

const Component = component ?? (wrapWithLabel ? 'label' : 'div');
const WrapperComponent = component ?? (wrapWithLabel ? 'label' : 'div');

checkedProps.checked = checkedProps.checked === null ? false : checkedProps.checked;
return (
<Component
className={css(styles.check, !label && styles.modifiers.standalone, className)}
htmlFor={wrapWithLabel ? props.id : undefined}
>
{labelPosition === 'start' ? (
<>
{labelRendered}
{inputRendered}
</>
) : (
<>
{inputRendered}
{labelRendered}
</>
)}
{description && (
<span id={this.state.descriptionId} className={css(styles.checkDescription)}>
{description}
</span>
)}
{body && <span className={css(styles.checkBody)}>{body}</span>}
</Component>
checkedProps.checked = checkedProps.checked === null ? false : checkedProps.checked;
return (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize checked before creating the input element.

On Line 158, checkedProps.checked is coerced after inputRendered is already built on Line 120, so isChecked === null can still pass checked={null} into <input>.

Proposed fix
-          const inputRendered = (
+          const normalizedCheckedProps = {
+            ...checkedProps,
+            ...(checkedProps.checked === null ? { checked: false } : {})
+          };
+
+          const inputRendered = (
             <input
               {...props}
               className={css(styles.checkInput, inputClassName)}
               type="checkbox"
               onChange={this.handleChange}
               aria-invalid={!isValid}
               aria-label={ariaLabel}
               aria-describedby={ariaDescribedByValue}
               disabled={isDisabled}
               required={isRequired}
               ref={(elem) => {
                 elem && (elem.indeterminate = isChecked === null);
               }}
-              {...checkedProps}
+              {...normalizedCheckedProps}
               {...getOUIAProps(Checkbox.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId, ouiaSafe)}
             />
           );
@@
-          checkedProps.checked = checkedProps.checked === null ? false : checkedProps.checked;
           return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const inputRendered = (
<input
{...props}
className={css(styles.checkInput, inputClassName)}
type="checkbox"
onChange={this.handleChange}
aria-invalid={!isValid}
aria-label={ariaLabel}
aria-describedby={ariaDescribedByValue}
disabled={isDisabled}
required={isRequired}
ref={(elem) => {
elem && (elem.indeterminate = isChecked === null);
}}
{...checkedProps}
{...getOUIAProps(Checkbox.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId, ouiaSafe)}
/>
);
const wrapWithLabel = (isLabelWrapped && !component) || component === 'label';
const wrapWithLabel = (isLabelWrapped && !component) || component === 'label';
const Label = wrapWithLabel ? 'span' : 'label';
const labelRendered = label ? (
<Label
className={css(styles.checkLabel, isDisabled && styles.modifiers.disabled)}
htmlFor={!wrapWithLabel ? props.id : undefined}
>
{label}
{isRequired && (
<span className={css(styles.checkLabelRequired)} aria-hidden="true">
{ASTERISK}
</span>
)}
</Label>
) : null;
const Label = wrapWithLabel ? 'span' : 'label';
const labelRendered = label ? (
<Label
className={css(styles.checkLabel, isDisabled && styles.modifiers.disabled)}
htmlFor={!wrapWithLabel ? props.id : undefined}
>
{label}
{isRequired && (
<span className={css(styles.checkLabelRequired)} aria-hidden="true">
{ASTERISK}
</span>
)}
</Label>
) : null;
const Component = component ?? (wrapWithLabel ? 'label' : 'div');
const WrapperComponent = component ?? (wrapWithLabel ? 'label' : 'div');
checkedProps.checked = checkedProps.checked === null ? false : checkedProps.checked;
return (
<Component
className={css(styles.check, !label && styles.modifiers.standalone, className)}
htmlFor={wrapWithLabel ? props.id : undefined}
>
{labelPosition === 'start' ? (
<>
{labelRendered}
{inputRendered}
</>
) : (
<>
{inputRendered}
{labelRendered}
</>
)}
{description && (
<span id={this.state.descriptionId} className={css(styles.checkDescription)}>
{description}
</span>
)}
{body && <span className={css(styles.checkBody)}>{body}</span>}
</Component>
checkedProps.checked = checkedProps.checked === null ? false : checkedProps.checked;
return (
const normalizedCheckedProps = {
...checkedProps,
...(checkedProps.checked === null ? { checked: false } : {})
};
const inputRendered = (
<input
{...props}
className={css(styles.checkInput, inputClassName)}
type="checkbox"
onChange={this.handleChange}
aria-invalid={!isValid}
aria-label={ariaLabel}
aria-describedby={ariaDescribedByValue}
disabled={isDisabled}
required={isRequired}
ref={(elem) => {
elem && (elem.indeterminate = isChecked === null);
}}
{...normalizedCheckedProps}
{...getOUIAProps(Checkbox.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId, ouiaSafe)}
/>
);
const wrapWithLabel = (isLabelWrapped && !component) || component === 'label';
const Label = wrapWithLabel ? 'span' : 'label';
const labelRendered = label ? (
<Label
className={css(styles.checkLabel, isDisabled && styles.modifiers.disabled)}
htmlFor={!wrapWithLabel ? props.id : undefined}
>
{label}
{isRequired && (
<span className={css(styles.checkLabelRequired)} aria-hidden="true">
{ASTERISK}
</span>
)}
</Label>
) : null;
const WrapperComponent = component ?? (wrapWithLabel ? 'label' : 'div');
return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Checkbox/Checkbox.tsx` around lines 120 -
159, The input may receive checked={null} because checkedProps.checked is
normalized after inputRendered is created; move the normalization so
checkedProps.checked is set (e.g., checkedProps.checked = checkedProps.checked
=== null ? false : checkedProps.checked) before building inputRendered (the JSX
that uses checkedProps), ensuring the ref indeterminate logic that reads
isChecked still works; update places referencing checkedProps, inputRendered,
isChecked, and WrapperComponent accordingly so the input never receives null for
the checked prop.

Comment on lines +68 to +72
{searchInput && (
<div className={css(styles.dualListSelectorToolsFilter)}>
{searchInput ? searchInput : <SearchInput isDisabled={isDisabled} />}
</div>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant conditional on line 70.

The inner ternary searchInput ? searchInput : <SearchInput isDisabled={isDisabled} /> is redundant because the outer condition on line 68 already checks for searchInput. If searchInput is truthy, it will always be rendered; the fallback <SearchInput /> can never execute.

Proposed fix
       {searchInput && (
         <div className={css(styles.dualListSelectorToolsFilter)}>
-          {searchInput ? searchInput : <SearchInput isDisabled={isDisabled} />}
+          {searchInput}
         </div>
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx`
around lines 68 - 72, The inner ternary is redundant: in DualListSelectorPane
remove the conditional expression "{searchInput ? searchInput : <SearchInput
isDisabled={isDisabled} />}" and simply render "searchInput" inside the div with
className={css(styles.dualListSelectorToolsFilter)} (or, if you intended to show
the default SearchInput when searchInput is falsy, change the outer condition to
always render the div and use the ternary there). Update references to
searchInput and SearchInput accordingly and keep isDisabled passed to the
default SearchInput if you choose the always-render approach.

Comment on lines +126 to +130
) : (
<span className={css(styles.switchToggle)}>
<div className={css(styles.switchToggleIcon)}>
<CheckIcon />
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Respect hasCheckIcon for unlabeled switches.

Line 126-Line 130 render CheckIcon unconditionally when label is omitted, so hasCheckIcon={false} is ignored in that path.

Proposed fix
-              ) : (
-                <span className={css(styles.switchToggle)}>
-                  <div className={css(styles.switchToggleIcon)}>
-                    <CheckIcon />
-                  </div>
-                </span>
-              )}
+              ) : (
+                <span className={css(styles.switchToggle)}>
+                  {hasCheckIcon && (
+                    <span className={css(styles.switchToggleIcon)}>
+                      <CheckIcon />
+                    </span>
+                  )}
+                </span>
+              )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Switch/Switch.tsx` around lines 126 - 130,
The Switch component currently renders <CheckIcon /> unconditionally in the
unlabeled branch, ignoring the hasCheckIcon prop; update the unlabeled rendering
path inside the Switch component to only render the CheckIcon when hasCheckIcon
is true (same condition used in the labeled branch), e.g. gate the element
rendered inside the node with className css(styles.switchToggleIcon) on
props.hasCheckIcon so that switchToggle and switchToggleIcon markup remains but
the icon is omitted when hasCheckIcon is false.

Comment on lines +15 to +17
: function useSSRSafeId(prefix = 'pf-'): string {
const [id] = useState(() => `${prefix}${++counter}`);
return id;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

React 17 fallback may still cause hydration mismatches.

The counter-based fallback for React 17 uses useState to stabilize the ID per-component, but the module-level counter will reset between server and client rendering. If this code runs on the server (SSR), IDs generated there won't match the client-side IDs since the counter starts fresh on both.

This fallback improves consistency within client-only rendering but doesn't fully solve SSR hydration for React 17. Consider documenting this limitation or logging a warning when the fallback is used in an SSR context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/helpers/useSSRSafeId.ts` around lines 15 - 17, The
counter-based fallback in useSSRSafeId can cause SSR hydration mismatches
because the module-level counter resets between server and client; update
useSSRSafeId to detect when the fallback path is taken (e.g., when useId is
unavailable) and when running on the server (typeof window === 'undefined') and
emit a dev-only warning (process.env.NODE_ENV !== 'production') that documents
the limitation and advises using React 18+ or explicit IDs; also add a short
note to the component library docs/README referencing useSSRSafeId and the SSR
limitation so consumers are aware.

Copy link
Contributor

@nicolethoen nicolethoen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this could result in snapshots being updated for some consumers, but I think they will change for the better.

I only have two questions related to some regressions flagged by coderabbit.

@dlabaj dlabaj self-assigned this Mar 2, 2026
@hemahg
Copy link

hemahg commented Mar 16, 2026

hi @arpanroy41 @dlabaj @nicolethoen thanks for working on this fix. This issue is currently blocking us from upgrading Next.js from 14 to 15 in our project. Next.js 14 has a vulnerability related to React Server Components, so we attempted the upgrade, but Next.js 15 introduces hydration errors with PatternFly.

Do you have an estimate of when this fix might be merged or released? It would help us plan our upgrade. Thanks!

@nicolethoen
Copy link
Contributor

we are happy to merge this once all review comments have been addressed. @arpanroy41 will you be able to address my comments?

…oved accessibility

- Changed the ouiaComponentType prop in FormSelect to utilize the component's displayName instead of a concatenated string with validation status.
- Updated the logic for modifying the existing aria attribute to use split and filter methods, ensuring that the specified ID is removed correctly.
@arpanroy41
Copy link
Contributor Author

@nicolethoen I have resolved the 2 points you have mentioned. Thank you for your help!

- Modified the ouiaComponentId in FormSelect snapshots to remove the "default" and validation status, ensuring a uniform format across all instances.
Copy link
Contributor

@dlabaj dlabaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dlabaj
Copy link
Contributor

dlabaj commented Mar 19, 2026

@arpanroy41 Looks like there's a conflict, but other then that it looks good to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple Components – Hydration errors in Next.js 15 (React 19)

5 participants