Skip to content

Fix/nextjs hydration errors 12250#12251

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

Fix/nextjs hydration errors 12250#12251
arpanroy41 wants to merge 9 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

Release Notes

  • Bug Fixes

    • Improved Server-Side Rendering (SSR) compatibility across core components with more stable and predictable element ID generation.
    • Enhanced accessibility by ensuring consistent ID generation in strict mode and SSR environments.
  • Refactor

    • Unified ID generation mechanism throughout the component library for better reliability.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Walkthrough

This PR systematically refactors ID generation across ~50 components and helper utilities to use SSR-safe patterns. It replaces unsafe ID generators (getUniqueId, getDefaultOUIAId) with new SSR-compatible utilities (useSSRSafeId hook and SSRSafeIds component), removes state-based ID tracking from class components, and simplifies render logic by eliminating wrapper patterns.

Changes

Cohort / File(s) Summary
New SSR-safe ID utilities
packages/react-core/src/helpers/useSSRSafeId.ts, packages/react-core/src/helpers/SSRSafeIds/SSRSafeIds.tsx, packages/react-core/src/helpers/index.ts
Introduces useSSRSafeId() hook for functional components and SSRSafeIds render-prop component for class components; both provide stable IDs across SSR/Strict Mode contexts. Updated helpers index to export new utilities.
GenerateId and OUIA refactoring
packages/react-core/src/helpers/GenerateId/GenerateId.ts, packages/react-core/src/helpers/OUIA/ouia.ts
Converts GenerateId from class to functional component using useSSRSafeId. Refactors useOUIAId to use useSSRSafeId and simplifies getDefaultOUIAId counter logic; adds variant parameter to useOUIAProps.
Functional components with useSSRSafeId
packages/react-core/src/components/CalendarMonth/CalendarMonth.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/JumpLinks/JumpLinks.tsx, packages/react-core/src/components/Popover/Popover.tsx, packages/react-core/src/components/Select/Select.tsx, packages/react-core/src/components/Tooltip/Tooltip.tsx, packages/react-core/src/components/Nav/NavGroup.tsx, packages/react-core/src/components/SearchInput/AdvancedSearchMenu.tsx, packages/react-core/src/deprecated/components/DualListSelector/*
Replace getUniqueId() calls with useSSRSafeId() hook to generate stable, SSR-safe IDs for component instances. Update parameter handling to use fallback pattern (prop or generated ID).
Functional components with GenerateId wrapper replacement
packages/react-core/src/components/AboutModal/AboutModal.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/ExpandableSection/ExpandableSection.tsx, packages/react-core/src/components/Form/FormGroup.tsx, packages/react-core/src/components/Form/InternalFormFieldGroup.tsx, packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx, packages/react-core/src/components/TimePicker/TimePicker.tsx, packages/react-core/src/components/TreeView/TreeViewListItem.tsx
Replace GenerateId wrapper pattern with direct useSSRSafeId() hook or inline GenerateId usage; simplify render structure and remove nested render-prop callbacks.
Class components with SSRSafeIds wrapper
packages/react-core/src/components/Card/CardHeader.tsx, packages/react-core/src/components/Checkbox/Checkbox.tsx, packages/react-core/src/components/FormSelect/FormSelect.tsx, packages/react-core/src/components/Menu/Menu.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/Radio/Radio.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/deprecated/components/Chip/Chip.tsx, packages/react-core/src/deprecated/components/Modal/Modal.tsx
Wrap component render with SSRSafeIds component to generate stable IDs; remove internal state for ID/OUIA tracking; update class signatures to remove state type parameters; simplify render logic.
Other component updates
packages/react-core/src/components/Menu/MenuItem.tsx, packages/react-core/src/components/Nav/Nav.tsx, packages/react-core/src/components/Nav/NavExpandable.tsx, packages/react-core/src/components/Progress/Progress.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
Mix of useSSRSafeId, GenerateId, or SSRSafeIds implementations; some components add state management (e.g., viewport width tracking) or lifecycle hooks; all maintain existing public API while refactoring ID generation.
Test 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
Remove getUniqueId mocking; relax ID assertion patterns from exact regex matches to looser patterns; update test IDs to reference correct generated identifiers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

This PR involves systematic refactoring across 50+ files with consistent but non-trivial pattern changes. While each individual file change follows predictable patterns (replacing ID generation approaches), the heterogeneity of changes (functional hooks, class wrappers, state removal, lifecycle adjustments) across diverse component types requires careful verification that ID generation semantics are preserved and SSR safety is achieved. Key risk areas include OUIA prop wiring, state removal correctness, and render-prop flow transitions.

Possibly related PRs

Suggested reviewers

  • nicolethoen
  • thatblindgeye
  • mcoker
  • lboehling
🚥 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 the PR, which is to fix Next.js hydration errors by replacing counter-based ID generation with React.useId() for deterministic SSR-safe IDs.
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
  • Post copyable unit tests in a comment

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.

@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 +76 to +77
<SSRSafeIds prefix="pf-" ouiaComponentType={`FormSelect${validated ? `-${validated}` : ''}`}>
{(_, generatedOuiaId) => (
Copy link

@coderabbitai coderabbitai bot Feb 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep generated OUIA IDs stable across validation state changes.

On Line 76, ouiaComponentType is tied to validated, so the generated OUIA id changes as validation changes. That makes data-ouia-component-id unstable for the same mounted control.

Proposed fix
-      <SSRSafeIds prefix="pf-" ouiaComponentType={`FormSelect${validated ? `-${validated}` : ''}`}>
+      <SSRSafeIds prefix="pf-" ouiaComponentType={FormSelect.displayName}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/FormSelect/FormSelect.tsx` around lines 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.

Copy link
Contributor

@nicolethoen nicolethoen Feb 26, 2026

Choose a reason for hiding this comment

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

CodeRsbbit's comment here is relevant.

The PR passes ouiaComponentType={\FormSelect${validated ?
-${validated} : ''}}toSSRSafeIds. Since validated can change at runtime,
the OUIA ID will change with it. The old code computed this once in the
constructor and stored it in state.

Copy link

Choose a reason for hiding this comment

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

@nicolethoen, thank you for confirming! You're absolutely right—this is a legitimate issue.

The problem: The current code passes a dynamic ouiaComponentType that includes the validated prop:

ouiaComponentType={`FormSelect${validated ? `-${validated}` : ''}`}

Since SSRSafeIds uses ouiaComponentType as part of the key for generating the OUIA ID (via useSSRSafeId), every time validated changes (e.g., from 'default''error''success'), a new OUIA ID is generated. This breaks the stability contract—OUIA IDs should remain constant for a mounted component instance.

The fix: Use the stable FormSelect.displayName instead:

ouiaComponentType={FormSelect.displayName}

This ensures the generated OUIA ID stays the same throughout the component's lifecycle, regardless of validation state changes. The validation state itself can still be exposed via other data attributes if needed for testing purposes, but shouldn't be part of the component type identifier.


✏️ Learnings added
Learnt from: nicolethoen
Repo: patternfly/patternfly-react PR: 12251
File: packages/react-core/src/components/FormSelect/FormSelect.tsx:76-77
Timestamp: 2026-02-26T22:33:56.823Z
Learning: In PatternFly React components using SSRSafeIds, the ouiaComponentType parameter must be a stable string (typically the component's displayName) that does not change during the component's lifecycle. Do not include dynamic props like validation state in ouiaComponentType, as this will cause the generated OUIA ID to change, breaking OUIA stability guarantees. Dynamic state should be exposed through separate data attributes if needed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: arpanroy41
Repo: patternfly/patternfly-react PR: 12237
File: packages/react-core/src/components/Toolbar/examples/ToolbarFlexGrowAndWidths.tsx:6-6
Timestamp: 2026-02-06T19:58:49.801Z
Learning: In the PatternFly React repository, React may be used without an explicit import in TSX files because React is provided globally by the build configuration. You can reference React types (e.g., React.FunctionComponent, React.ReactNode) without importing React. Do not add 'import React from 'react'' in TSX files; rely on the global React availability.

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 +166 to +167
const generatedId = useSSRSafeId('pf-tooltip-');
const id = idProp ?? generatedId;
Copy link

@coderabbitai coderabbitai bot Feb 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/react-core/src/components/Tooltip/Tooltip.tsx | head -200 | tail -50

Repository: patternfly/patternfly-react

Length of output: 2213


🏁 Script executed:

# Search for the cleanup logic mentioned at line 213
rg -n "removeAriaFromRefElement\|replace.*RegExp.*\\\\b.*id" packages/react-core/src/components/Tooltip/Tooltip.tsx -B2 -A2

Repository: patternfly/patternfly-react

Length of output: 53


🏁 Script executed:

# Find the useSSRSafeId hook to understand what format it returns
fd useSSRSafeId packages/react-core -t ts -t tsx

Repository: patternfly/patternfly-react

Length of output: 240


🏁 Script executed:

# Check what useSSRSafeId actually does
rg -n "useSSRSafeId" packages/react-core/src -B2 -A5 -t ts -t tsx | head -100

Repository: patternfly/patternfly-react

Length of output: 98


🏁 Script executed:

# Read around line 213 to see the cleanup logic
sed -n '200,225p' packages/react-core/src/components/Tooltip/Tooltip.tsx

Repository: patternfly/patternfly-react

Length of output: 740


🏁 Script executed:

# Search for removeAriaFromRefElement function
rg -n "removeAriaFromRefElement" packages/react-core/src/components/Tooltip/Tooltip.tsx -A10

Repository: patternfly/patternfly-react

Length of output: 765


🏁 Script executed:

# Find useSSRSafeId definition
fd useSSRSafeId packages/react-core/src

Repository: patternfly/patternfly-react

Length of output: 118


🏁 Script executed:

cat packages/react-core/src/helpers/useSSRSafeId.ts

Repository: patternfly/patternfly-react

Length of output: 703


🏁 Script executed:

# Search for React useId usage in the codebase to understand the ID format
rg "useId\|generatedId" packages/react-core/src/helpers/useSSRSafeId.ts -B5 -A5

Repository: patternfly/patternfly-react

Length of output: 53


🏁 Script executed:

# Test the regex behavior with useId-style IDs
python <<'PY'
import re

# Simulate React useId format
id_value = "pf-tooltip-:r0:"
existing_aria = f"other-tooltip {id_value} some-id"

# Current regex pattern from line 213
pattern = rf"\b{id_value}\b"
result = existing_aria.replace(re.compile(pattern, re.MULTILINE), '', 1).strip()

print(f"ID value: {id_value}")
print(f"Input aria: {existing_aria}")
print(f"Pattern: {pattern}")
print(f"After cleanup: {result}")
print(f"Token removed: {id_value not in result}")
print()

# Let's also test with the proposed fix
def split_filter_approach(existing_aria, id_value):
    return ' '.join(token for token in existing_aria.split() if token and token != id_value)

result2 = split_filter_approach(existing_aria, id_value)
print(f"With proposed fix: {result2}")
print(f"Token removed (proposed): {id_value not in result2}")
PY

Repository: patternfly/patternfly-react

Length of output: 203


🏁 Script executed:

# Also test React 17 fallback format (counter-based)
python <<'PY'
import re

# React 17 fallback format (simple counter)
id_value_simple = "pf-tooltip-1"
existing_aria = f"other-tooltip {id_value_simple} some-id"

# Current regex pattern
pattern = rf"\b{id_value_simple}\b"
result = re.sub(pattern, '', existing_aria).strip()

print(f"Simple ID (React 17 fallback): {id_value_simple}")
print(f"Input aria: {existing_aria}")
print(f"After cleanup: {result}")
print(f"Token removed: {id_value_simple not in result}")
PY

Repository: patternfly/patternfly-react

Length of output: 219


🏁 Script executed:

# Test the regex behavior with useId-style IDs (corrected)
python <<'PY'
import re

# Simulate React useId format
id_value = "pf-tooltip-:r0:"
existing_aria = f"other-tooltip {id_value} some-id"

# Current regex pattern from line 213 using re.sub (not replace)
pattern = rf"\b{id_value}\b"
result = re.sub(pattern, '', existing_aria).strip()

print(f"ID value: {id_value}")
print(f"Input aria: {existing_aria}")
print(f"Pattern: {pattern}")
print(f"After cleanup with \b: {result}")
print(f"Token removed: {id_value not in result}")
print()

# Let's test with escaped id
escaped_id = re.escape(id_value)
pattern_escaped = rf"\b{escaped_id}\b"
result_escaped = re.sub(pattern_escaped, '', existing_aria).strip()

print(f"Pattern with escaped ID: {pattern_escaped}")
print(f"After cleanup with escaped \b: {result_escaped}")
print(f"Token removed (escaped): {id_value not in result_escaped}")
print()

# Test the proposed fix
def split_filter_approach(existing_aria, id_value):
    return ' '.join(token for token in existing_aria.split() if token and token != id_value)

result2 = split_filter_approach(existing_aria, id_value)
print(f"With proposed fix: {result2}")
print(f"Token removed (proposed): {id_value not in result2}")
PY

Repository: patternfly/patternfly-react

Length of output: 476


useId-style IDs break aria cleanup due to \b word boundary mismatch.

With React 18+ using useId(), generated IDs like pf-tooltip-:r0: cause the cleanup regex at Line 213 to fail. Word boundaries (\b) don't match correctly when IDs contain colons (non-word characters), leaving stale aria-describedby / aria-labelledby attributes on the trigger element. This breaks accessibility for screen reader users.

🔧 Proposed fix
-    const newAria = existingAria.replace(new RegExp(`\\b${id}\\b`, 'g'), '').trim();
+    const newAria = existingAria
+      .split(/\s+/)
+      .filter((token) => token && token !== id)
+      .join(' ');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Tooltip/Tooltip.tsx` around lines 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.

Copy link
Contributor

@nicolethoen nicolethoen Feb 26, 2026

Choose a reason for hiding this comment

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

CodeRsbbit's comment here is relevant.

The removeAriaFromRefElement function uses \b word
boundaries to strip the tooltip's ID from aria-describedby. Colons in useId()
IDs (:r0:) are non-word characters, so the regex will fail to match, leaving
stale IDs in aria attributes.

I recommend replacing the regex with split/filter/join:
old

const newAria = existingAria.replace(new RegExp(`\\b${id}\\b`, 'g'),  '').trim();

New

const newAria = existingAria.split(/\s+/).filter((token) => token && token !== id).join(' ');

Copy link

Choose a reason for hiding this comment

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

@nicolethoen, thank you for confirming the issue and providing the recommended fix! You're absolutely right—the split/filter/join approach is much more robust and avoids the regex special character issues entirely.

Would you like me to implement this fix? I can update the removeAriaFromRefElement function to use the split/filter/join approach you've recommended, which will properly handle useId-style IDs with colons.

Task: Fix aria cleanup for useId-style IDs in Tooltip

  • Update removeAriaFromRefElement function to use split/filter/join instead of regex with \b word boundaries
  • This ensures proper cleanup of aria attributes when IDs contain special characters like colons

🧠 Learnings used
Learnt from: arpanroy41
Repo: patternfly/patternfly-react PR: 12237
File: packages/react-core/src/components/Toolbar/examples/ToolbarFlexGrowAndWidths.tsx:6-6
Timestamp: 2026-02-06T19:58:49.801Z
Learning: In the PatternFly React repository, React may be used without an explicit import in TSX files because React is provided globally by the build configuration. You can reference React types (e.g., React.FunctionComponent, React.ReactNode) without importing React. Do not add 'import React from 'react'' in TSX files; rely on the global React availability.

💡 For code changes across multiple files, use @coderabbitai plan in an issue comment

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.

Comment on lines +166 to +167
const generatedId = useSSRSafeId('pf-tooltip-');
const id = idProp ?? generatedId;
Copy link
Contributor

@nicolethoen nicolethoen Feb 26, 2026

Choose a reason for hiding this comment

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

CodeRsbbit's comment here is relevant.

The removeAriaFromRefElement function uses \b word
boundaries to strip the tooltip's ID from aria-describedby. Colons in useId()
IDs (:r0:) are non-word characters, so the regex will fail to match, leaving
stale IDs in aria attributes.

I recommend replacing the regex with split/filter/join:
old

const newAria = existingAria.replace(new RegExp(`\\b${id}\\b`, 'g'),  '').trim();

New

const newAria = existingAria.split(/\s+/).filter((token) => token && token !== id).join(' ');

Comment on lines +76 to +77
<SSRSafeIds prefix="pf-" ouiaComponentType={`FormSelect${validated ? `-${validated}` : ''}`}>
{(_, generatedOuiaId) => (
Copy link
Contributor

@nicolethoen nicolethoen Feb 26, 2026

Choose a reason for hiding this comment

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

CodeRsbbit's comment here is relevant.

The PR passes ouiaComponentType={\FormSelect${validated ?
-${validated} : ''}}toSSRSafeIds. Since validated can change at runtime,
the OUIA ID will change with it. The old code computed this once in the
constructor and stored it in state.

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)

3 participants