From 4c2771fd0cc3b76db5a8d35cc43bd3e05401a165 Mon Sep 17 00:00:00 2001
From: Douglas Fabris <devfabris@gmail.com>
Date: Mon, 22 Jan 2024 13:58:33 -0300
Subject: [PATCH] feat: Composer keyboard navigability (#31510)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
---
 .changeset/four-eels-compete.md               |  8 +++++
 apps/meteor/package.json                      |  1 +
 .../meteor/tests/e2e/message-composer.spec.ts | 29 +++++++++++++++++--
 .../tests/e2e/page-objects/home-channel.ts    |  6 +++-
 packages/ui-composer/package.json             |  5 ++--
 .../MessageComposerToolbarActions.tsx         | 16 +++++++---
 yarn.lock                                     |  2 ++
 7 files changed, 56 insertions(+), 11 deletions(-)
 create mode 100644 .changeset/four-eels-compete.md

diff --git a/.changeset/four-eels-compete.md b/.changeset/four-eels-compete.md
new file mode 100644
index 00000000000..65b2d0c495f
--- /dev/null
+++ b/.changeset/four-eels-compete.md
@@ -0,0 +1,8 @@
+---
+'@rocket.chat/ui-composer': minor
+'@rocket.chat/meteor': minor
+---
+
+Composer keyboard navigability 
+
+![Kapture 2024-01-22 at 11 33 14](https://github.com/RocketChat/Rocket.Chat/assets/27704687/f116c1e6-4ec7-4175-a01b-fa98eade2416)
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 008d757c3a8..4a7ee0f1147 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -222,6 +222,7 @@
 		"@nivo/line": "0.84.0",
 		"@nivo/pie": "0.84.0",
 		"@react-aria/color": "^3.0.0-beta.15",
+		"@react-aria/toolbar": "^3.0.0-beta.1",
 		"@react-pdf/renderer": "^3.1.14",
 		"@rocket.chat/account-utils": "workspace:^",
 		"@rocket.chat/agenda": "workspace:^",
diff --git a/apps/meteor/tests/e2e/message-composer.spec.ts b/apps/meteor/tests/e2e/message-composer.spec.ts
index 861ee17de21..9ed3e42b941 100644
--- a/apps/meteor/tests/e2e/message-composer.spec.ts
+++ b/apps/meteor/tests/e2e/message-composer.spec.ts
@@ -5,7 +5,7 @@ import { expect, test } from './utils/test';
 
 test.use({ storageState: Users.user1.state });
 
-test.describe.serial('Composer', () => {
+test.describe.serial('message-composer', () => {
 	let poHomeChannel: HomeChannel;
 	let targetChannel: string;
 
@@ -23,7 +23,7 @@ test.describe.serial('Composer', () => {
 		await poHomeChannel.sidenav.openChat(targetChannel);
 		await poHomeChannel.content.sendMessage('hello composer');
 
-		await expect(poHomeChannel.composerToolboxActions).toHaveCount(11);
+		await expect(poHomeChannel.composerToolbarActions).toHaveCount(11);
 	});
 
 	test('should have only the main formatter and the main action', async ({ page }) => {
@@ -32,6 +32,29 @@ test.describe.serial('Composer', () => {
 		await poHomeChannel.sidenav.openChat(targetChannel);
 		await poHomeChannel.content.sendMessage('hello composer');
 
-		await expect(poHomeChannel.composerToolboxActions).toHaveCount(5);
+		await expect(poHomeChannel.composerToolbarActions).toHaveCount(5);
+	});
+
+	test('should navigate on toolbar using arrow keys', async ({ page }) => {
+		await poHomeChannel.sidenav.openChat(targetChannel);
+		await poHomeChannel.content.sendMessage('hello composer');
+
+		await page.keyboard.press('Tab');
+		await page.keyboard.press('ArrowRight');
+		await page.keyboard.press('ArrowRight');
+		await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Italic' })).toBeFocused();
+
+		await page.keyboard.press('ArrowLeft');
+		await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Bold' })).toBeFocused();
+	});
+
+	test('should move the focus away from toolbar using tab key', async ({ page }) => {
+		await poHomeChannel.sidenav.openChat(targetChannel);
+		await poHomeChannel.content.sendMessage('hello composer');
+
+		await page.keyboard.press('Tab');
+		await page.keyboard.press('Tab');
+		
+		await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Emoji' })).not.toBeFocused();
 	});
 });
diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts
index 8cc5d324569..78b044fa48f 100644
--- a/apps/meteor/tests/e2e/page-objects/home-channel.ts
+++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts
@@ -41,7 +41,11 @@ export class HomeChannel {
 		await this.page.mouse.move(0, 0);
 	}
 
-	get composerToolboxActions(): Locator {
+	get composerToolbar(): Locator {
+		return this.page.locator('[role=toolbar][aria-label="Composer Primary Actions"]');
+	}
+
+	get composerToolbarActions(): Locator {
 		return this.page.locator('[role=toolbar][aria-label="Composer Primary Actions"] button');
 	}
 }
diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json
index b3eb0ce53bd..076ed68a08e 100644
--- a/packages/ui-composer/package.json
+++ b/packages/ui-composer/package.json
@@ -4,6 +4,7 @@
   "private": true,
   "devDependencies": {
     "@babel/core": "~7.22.20",
+    "@react-aria/toolbar": "^3.0.0-beta.1",
     "@rocket.chat/eslint-config": "workspace:^",
     "@rocket.chat/fuselage": "^0.44.2",
     "@rocket.chat/icons": "^0.33.0",
@@ -26,6 +27,7 @@
     "typescript": "~5.3.2"
   },
   "peerDependencies": {
+    "@react-aria/toolbar": "*",
     "@rocket.chat/fuselage": "*",
     "@rocket.chat/icons": "*",
     "react": "^17.0.2",
@@ -46,8 +48,5 @@
   ],
   "volta": {
     "extends": "../../package.json"
-  },
-  "dependencies": {
-    "@react-aria/toolbar": "^3.0.0-beta.1"
   }
 }
diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx
index 23edd5e5d0f..bc462178c7f 100644
--- a/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx
+++ b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx
@@ -1,8 +1,16 @@
+import { useToolbar } from '@react-aria/toolbar';
 import { ButtonGroup } from '@rocket.chat/fuselage';
-import type { ComponentProps, ReactElement } from 'react';
+import { useRef, type ComponentProps, type ReactElement } from 'react';
 
-const MessageComposerToolbarActions = (props: ComponentProps<typeof ButtonGroup>): ReactElement => (
-	<ButtonGroup role='toolbar' small {...props} />
-);
+const MessageComposerToolbarActions = (props: ComponentProps<typeof ButtonGroup>): ReactElement => {
+	const ref = useRef(null);
+	const { toolbarProps } = useToolbar(props, ref);
+
+	return (
+		<ButtonGroup role='toolbar' small ref={ref} {...toolbarProps}>
+			{props.children}
+		</ButtonGroup>
+	);
+};
 
 export default MessageComposerToolbarActions;
diff --git a/yarn.lock b/yarn.lock
index d7ec705086e..535a69f809a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9591,6 +9591,7 @@ __metadata:
     "@nivo/pie": 0.84.0
     "@playwright/test": ^1.40.1
     "@react-aria/color": ^3.0.0-beta.15
+    "@react-aria/toolbar": ^3.0.0-beta.1
     "@react-pdf/renderer": ^3.1.14
     "@rocket.chat/account-utils": "workspace:^"
     "@rocket.chat/agenda": "workspace:^"
@@ -10521,6 +10522,7 @@ __metadata:
     ts-jest: ~29.1.1
     typescript: ~5.3.2
   peerDependencies:
+    "@react-aria/toolbar": "*"
     "@rocket.chat/fuselage": "*"
     "@rocket.chat/icons": "*"
     react: ^17.0.2
-- 
GitLab