Add FocusLock to emoji picker (#31146)

* Add focus lock to emoji picker and e2e test.

* Remove direct use of FocusLock in favour of the ContextMenu prop.

* Apply returnFocus for ContextMenu focusLocks

* Remove import
This commit is contained in:
David Langley
2025-10-31 13:54:26 +00:00
committed by GitHub
parent 23f372ca08
commit e0a94a05ea
6 changed files with 40 additions and 5 deletions

View File

@@ -92,6 +92,41 @@ test.describe("Composer", () => {
});
});
test("should have focus lock in emoji picker", async ({ page, app }) => {
const emojiButton = app.getComposer(false).getByRole("button", { name: "Emoji" });
// Open emoji picker by clicking the button
await emojiButton.click();
// Wait for emoji picker to be visible
const emojiPicker = page.getByTestId("mx_EmojiPicker");
await expect(emojiPicker).toBeVisible();
// Get initial focused element (should be search input)
const searchInput = emojiPicker.getByRole("textbox", { name: "Search" });
await expect(searchInput).toBeFocused();
// Try to tab multiple times - focus should stay within emoji picker
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
// Verify we're still within the emoji picker (not back to composer)
const focusedElement = await page.evaluate(() => document.activeElement?.closest(".mx_EmojiPicker"));
expect(focusedElement).not.toBeNull();
// Close with Escape key
await page.keyboard.press("Escape");
// Verify emoji picker is closed
await expect(emojiPicker).not.toBeVisible();
// Verify focus returns to emoji button
await expect(emojiButton).toBeFocused();
});
test.describe("when Control+Enter is required to send", () => {
test.beforeEach(async ({ app }) => {
await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);

View File

@@ -405,7 +405,7 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
);
if (focusLock) {
body = <FocusLock>{body}</FocusLock>;
body = <FocusLock returnFocus>{body}</FocusLock>;
}
// filter props that are invalid for DOM elements

View File

@@ -735,7 +735,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
if (this.state.reactionPickerDisplayed) {
const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
reactionPicker = (
<ContextMenu {...toRightOf(buttonRect)} onFinished={this.closeMenu} managed={false}>
<ContextMenu {...toRightOf(buttonRect)} onFinished={this.closeMenu} managed={false} focusLock>
<ReactionPicker mxEvent={mxEvent} onFinished={this.onCloseReactionPicker} reactions={reactions} />
</ContextMenu>
);

View File

@@ -156,7 +156,7 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
if (menuDisplayed && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false} focusLock>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>
);

View File

@@ -34,7 +34,7 @@ const ReactButton: React.FC<IProps> = ({ mxEvent, reactions }) => {
if (menuDisplayed && button.current) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false} focusLock>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>
);

View File

@@ -34,7 +34,7 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
};
contextMenu = (
<ContextMenu {...position} onFinished={onFinished} managed={false}>
<ContextMenu {...position} onFinished={onFinished} managed={false} focusLock>
<EmojiPicker onChoose={addEmoji} onFinished={onFinished} />
</ContextMenu>
);