diff --git a/apps/react-17-tests-v9/config/support/commands.js b/apps/react-17-tests-v9/config/support/commands.js
new file mode 100644
index 0000000000000..045dab2b3d635
--- /dev/null
+++ b/apps/react-17-tests-v9/config/support/commands.js
@@ -0,0 +1,7 @@
+import { realPress } from 'cypress-real-events/commands/realPress';
+
+/**
+ * Press command fallback for Cypress 13 compatibility.
+ * The press command is available in Cypress 14+ but not in Cypress 13.
+ */
+Cypress.Commands.add('press', realPress);
diff --git a/apps/react-17-tests-v9/config/support/component.js b/apps/react-17-tests-v9/config/support/component.js
new file mode 100644
index 0000000000000..b74bc56b30466
--- /dev/null
+++ b/apps/react-17-tests-v9/config/support/component.js
@@ -0,0 +1,5 @@
+// base config support file
+import '../../../../scripts/cypress/src/support/component';
+
+// custom commands
+import './commands';
diff --git a/apps/react-17-tests-v9/cypress-react-17.config.ts b/apps/react-17-tests-v9/cypress-react-17.config.ts
index dd61ebda49147..06c1796e59ba7 100644
--- a/apps/react-17-tests-v9/cypress-react-17.config.ts
+++ b/apps/react-17-tests-v9/cypress-react-17.config.ts
@@ -13,7 +13,13 @@ const specs = [
path.resolve('../../packages/react-components/react-tabster/src/**/*.cy.{tsx,ts}'),
...excludedSpecs,
];
-const config = { ...baseConfig };
+const config = {
+ ...baseConfig,
+ component: {
+ ...baseConfig.component,
+ supportFile: path.resolve(__dirname, 'config/support/component.js'),
+ },
+};
config.component.specPattern = specs;
config.component.devServer.webpackConfig.resolve ??= {};
diff --git a/change/@fluentui-react-utilities-649e1127-b2c6-4503-861d-4c57f42bf7ad.json b/change/@fluentui-react-utilities-649e1127-b2c6-4503-861d-4c57f42bf7ad.json
new file mode 100644
index 0000000000000..87af57f2b7f69
--- /dev/null
+++ b/change/@fluentui-react-utilities-649e1127-b2c6-4503-861d-4c57f42bf7ad.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: enhance hook to support React concurrent mode using setEffect for first mount tracking",
+ "packageName": "@fluentui/react-utilities",
+ "email": "dmytrokirpa@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-drawer/library/src/components/DrawerFooter/DrawerFooter.cy.tsx b/packages/react-components/react-drawer/library/src/components/DrawerFooter/DrawerFooter.cy.tsx
index 7ed078e81592a..fdef24f4b50c4 100644
--- a/packages/react-components/react-drawer/library/src/components/DrawerFooter/DrawerFooter.cy.tsx
+++ b/packages/react-components/react-drawer/library/src/components/DrawerFooter/DrawerFooter.cy.tsx
@@ -43,7 +43,7 @@ describe('DrawerFooter', () => {
cy.get('#drawer-body').scrollTo('center');
cy.get('#drawer-footer').within($el => {
- cy.window().then(win => {
+ cy.window().should(win => {
const before = win.getComputedStyle($el[0], '::before');
const opacity = before.getPropertyValue('opacity');
diff --git a/packages/react-components/react-tabster/src/useKeyborgRef.cy.tsx b/packages/react-components/react-tabster/src/useKeyborgRef.cy.tsx
index 13acbbda3d826..fe04dcc34f562 100644
--- a/packages/react-components/react-tabster/src/useKeyborgRef.cy.tsx
+++ b/packages/react-components/react-tabster/src/useKeyborgRef.cy.tsx
@@ -23,12 +23,12 @@ describe('Keyborg', () => {
it('should dispose keyborg instance on unmount', () => {
mount();
- cy.window().then(win => {
+ cy.window().should(win => {
// @ts-expect-error - Only way to definitively check if keyborg is disposed
expect(win.__keyborg).not.equals(undefined);
});
mount(
Unmounted
);
- cy.window().then(win => {
+ cy.window().should(win => {
// @ts-expect-error - Only way to definitively check if keyborg is disposed
expect(win.__keyborg).equals(undefined);
});
diff --git a/packages/react-components/react-tree/library/src/components/FlatTree/FlatTree.cy.tsx b/packages/react-components/react-tree/library/src/components/FlatTree/FlatTree.cy.tsx
index a20bcacb14af9..3b08192ba9fb5 100644
--- a/packages/react-components/react-tree/library/src/components/FlatTree/FlatTree.cy.tsx
+++ b/packages/react-components/react-tree/library/src/components/FlatTree/FlatTree.cy.tsx
@@ -203,9 +203,9 @@ describe('FlatTree', () => {
,
);
cy.focused().should('not.exist');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('#action').should('be.focused');
});
describe('navigationMode="treegrid"', () => {
@@ -241,9 +241,9 @@ describe('FlatTree', () => {
,
);
cy.focused().should('not.exist');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('#action').should('be.focused').realPress('{enter}');
cy.get('[data-testid="item1__item1"]').should('not.exist');
cy.get('#action').should('be.focused').realPress('Space');
@@ -252,15 +252,15 @@ describe('FlatTree', () => {
it('should focus on first item when pressing tab key', () => {
mount();
cy.focused().should('not.exist');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
});
it('should focus out of tree when pressing tab key inside tree.', () => {
mount();
cy.focused().should('not.exist');
- cy.document().realPress('Tab');
+ cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
- cy.focused().realPress('Tab');
+ cy.focused().press('Tab');
cy.focused().should('not.exist');
});
describe('Navigation', () => {
@@ -268,7 +268,7 @@ describe('FlatTree', () => {
mount();
cy.get('[data-testid="item1"]').focus().realPress('{downarrow}');
cy.get('[data-testid="item2"]').should('be.focused');
- cy.focused().realPress('Tab').should('not.exist');
+ cy.focused().press('Tab').should('not.exist');
});
describe('navigationMode="treegrid"', () => {
it('should move with Up/Down keys', () => {
@@ -434,7 +434,7 @@ describe('FlatTree', () => {
defaultCheckedItems={['item1__item1']}
/>,
);
- cy.window().then(win => {
+ cy.window().should(win => {
expect(win.console.error).to.be.called;
});
});
diff --git a/packages/react-components/react-utilities/src/hooks/useFirstMount.ts b/packages/react-components/react-utilities/src/hooks/useFirstMount.ts
index 0c046529894d5..68a3c05fd2a62 100644
--- a/packages/react-components/react-utilities/src/hooks/useFirstMount.ts
+++ b/packages/react-components/react-utilities/src/hooks/useFirstMount.ts
@@ -3,10 +3,8 @@ import * as React from 'react';
/**
* @internal
* Checks if components was mounted the first time.
- * Since concurrent mode will be released in the future this needs to be verified
- * Currently (React 17) will always render the initial mount once
- * https://codesandbox.io/s/heuristic-brook-s4w0q?file=/src/App.jsx
- * https://codesandbox.io/s/holy-grass-8nieu?file=/src/App.jsx
+ * Supports React concurrent/strict mode by using `useEffect`
+ * to track the first mount instead of mutating refs during render.
*
* @example
* const isFirstMount = useFirstMount();
@@ -14,10 +12,11 @@ import * as React from 'react';
export function useFirstMount(): boolean {
const isFirst = React.useRef(true);
- if (isFirst.current) {
- isFirst.current = false;
- return true;
- }
+ React.useEffect(() => {
+ if (isFirst.current) {
+ isFirst.current = false;
+ }
+ }, []);
return isFirst.current;
}