From 13c78ea9e703ced01dab9ed5b13e4876ad82ff9f Mon Sep 17 00:00:00 2001 From: "manuel.carrera" Date: Tue, 16 Dec 2025 09:41:19 -0700 Subject: [PATCH 1/3] fix: Update useUniqueId to use unicode safe selector --- jest/setupTests.ts | 2 +- modules/react/common/lib/utils/useUniqueId.ts | 10 ++- modules/react/menu/spec/Menu.spec.tsx | 63 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 modules/react/menu/spec/Menu.spec.tsx diff --git a/jest/setupTests.ts b/jest/setupTests.ts index 8c3fc76b71..ffb96f2b4a 100644 --- a/jest/setupTests.ts +++ b/jest/setupTests.ts @@ -1,4 +1,4 @@ -import '@testing-library/jest-dom/extend-expect'; +import '@testing-library/jest-dom'; import {verifyComponent} from './verifyComponent'; import {jest} from '@jest/globals'; import {ResizeObserver} from '@juggle/resize-observer'; diff --git a/modules/react/common/lib/utils/useUniqueId.ts b/modules/react/common/lib/utils/useUniqueId.ts index f3ca7c1201..85961f7aa2 100644 --- a/modules/react/common/lib/utils/useUniqueId.ts +++ b/modules/react/common/lib/utils/useUniqueId.ts @@ -26,12 +26,20 @@ export const generateUniqueId = () => seed + (c++).toString(36); /** * Generate a unique ID if one is not provided. The generated ID will be stable across renders. Uses * `React.useId()` if available. + * + * Note: React's `useId()` generates IDs with colons (e.g., `:r0:`), which are not valid in CSS + * selectors. We transform to use unicode guillemets (`«r0»`) matching React's upcoming format + * change (https://github.com/facebook/react/pull/32001). + * * @param id Optional ID provided that will be used instead of a unique ID */ export const useUniqueId = (id?: string) => { // https://codesandbox.io/s/react-functional-component-ids-p2ndq // eslint-disable-next-line react-hooks/rules-of-hooks - const generatedId = hasStableId ? React.useId() : useConstant(generateUniqueId); + const reactId = hasStableId ? React.useId() : useConstant(generateUniqueId); + // Transform React's useId format (:r0:) to CSS-safe format («r0») + // This matches React's upcoming format: https://github.com/facebook/react/pull/32001 + const generatedId = hasStableId ? reactId.replace(/^:/, '«').replace(/:$/, '»') : reactId; return id || generatedId; }; diff --git a/modules/react/menu/spec/Menu.spec.tsx b/modules/react/menu/spec/Menu.spec.tsx new file mode 100644 index 0000000000..583703fa7e --- /dev/null +++ b/modules/react/menu/spec/Menu.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import {render, screen, fireEvent, waitFor} from '@testing-library/react'; +import {Basic} from '../stories/examples/Basic'; + +describe('Menu with RTL queries (verifying useId fix)', () => { + const BasicMenu = () => { + return ; + }; + + it('should work with getByText and fireEvent.click', async () => { + render(); + + // Click the menu target using getByText - this would fail with :r0: IDs + const target = screen.getByText('Open Menu'); + fireEvent.click(target); + + // Find menu item using getByText + const firstItem = await screen.findByText('First Item'); + fireEvent.click(firstItem); + + // Verify selection worked + await waitFor(() => { + expect(screen.getByTestId('output')).toHaveTextContent('0'); + }); + }); + + it('should work with getByRole queries', async () => { + render(); + + // Use role-based query + const target = screen.getByRole('button', {name: 'Open Menu'}); + fireEvent.click(target); + + // Menu items have menuitem role + const secondItem = await screen.findByRole('menuitem', {name: 'Second Item'}); + fireEvent.click(secondItem); + + await waitFor(() => { + expect(screen.getByTestId('output')).toHaveTextContent('1'); + }); + }); + + it('should generate CSS-safe IDs with guillemets', async () => { + const MenuWithGroup = () => ; + + render(); + + // Open the menu to render the group + fireEvent.click(screen.getByText('Open Menu')); + + // Menu.Group uses useUniqueId for its heading ID + const groupHeading = await screen.findByText('Group Heading'); + const id = groupHeading.id; + + // ID should use guillemets «» not colons : + // Format should be like «r0» not :r0: + expect(id).not.toContain(':'); + expect(id).toMatch(/^«.*»$/); // Should start with « and end with » + + // Verify it's a valid CSS selector (the main issue we're fixing) + expect(() => document.querySelector(`[id="${id}"]`)).not.toThrow(); + }); +}); From df55b83a0586e7f855928ec939975acfb91b7677 Mon Sep 17 00:00:00 2001 From: Manuel Carrera Date: Tue, 16 Dec 2025 11:54:34 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Alan B Smith --- modules/react/common/lib/utils/useUniqueId.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/react/common/lib/utils/useUniqueId.ts b/modules/react/common/lib/utils/useUniqueId.ts index 85961f7aa2..87d3b1bf06 100644 --- a/modules/react/common/lib/utils/useUniqueId.ts +++ b/modules/react/common/lib/utils/useUniqueId.ts @@ -27,7 +27,7 @@ export const generateUniqueId = () => seed + (c++).toString(36); * Generate a unique ID if one is not provided. The generated ID will be stable across renders. Uses * `React.useId()` if available. * - * Note: React's `useId()` generates IDs with colons (e.g., `:r0:`), which are not valid in CSS + * Note: In React 18, `useId()` generates IDs with colons (e.g., `:r0:`), which are not valid in CSS * selectors. We transform to use unicode guillemets (`«r0»`) matching React's upcoming format * change (https://github.com/facebook/react/pull/32001). * @@ -38,7 +38,7 @@ export const useUniqueId = (id?: string) => { // eslint-disable-next-line react-hooks/rules-of-hooks const reactId = hasStableId ? React.useId() : useConstant(generateUniqueId); // Transform React's useId format (:r0:) to CSS-safe format («r0») - // This matches React's upcoming format: https://github.com/facebook/react/pull/32001 + // This matches React 19's [format](https://github.com/facebook/react/pull/32001). When we bump to >= React 19.1.0, we can remove this logic and use `useId()` directly. const generatedId = hasStableId ? reactId.replace(/^:/, '«').replace(/:$/, '»') : reactId; return id || generatedId; }; From 060f7aa3d689b26198365d91a4d76ec23a947da2 Mon Sep 17 00:00:00 2001 From: "manuel.carrera" Date: Tue, 16 Dec 2025 13:55:13 -0700 Subject: [PATCH 3/3] test: Remove unecessary test --- modules/react/menu/spec/Menu.spec.tsx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/modules/react/menu/spec/Menu.spec.tsx b/modules/react/menu/spec/Menu.spec.tsx index 583703fa7e..a34e2f7ea1 100644 --- a/modules/react/menu/spec/Menu.spec.tsx +++ b/modules/react/menu/spec/Menu.spec.tsx @@ -39,25 +39,4 @@ describe('Menu with RTL queries (verifying useId fix)', () => { expect(screen.getByTestId('output')).toHaveTextContent('1'); }); }); - - it('should generate CSS-safe IDs with guillemets', async () => { - const MenuWithGroup = () => ; - - render(); - - // Open the menu to render the group - fireEvent.click(screen.getByText('Open Menu')); - - // Menu.Group uses useUniqueId for its heading ID - const groupHeading = await screen.findByText('Group Heading'); - const id = groupHeading.id; - - // ID should use guillemets «» not colons : - // Format should be like «r0» not :r0: - expect(id).not.toContain(':'); - expect(id).toMatch(/^«.*»$/); // Should start with « and end with » - - // Verify it's a valid CSS selector (the main issue we're fixing) - expect(() => document.querySelector(`[id="${id}"]`)).not.toThrow(); - }); });