diff --git a/.changeset/multiselect-toggle-reopen.md b/.changeset/multiselect-toggle-reopen.md
new file mode 100644
index 0000000000..18abb11dd6
--- /dev/null
+++ b/.changeset/multiselect-toggle-reopen.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/fuselage': patch
+---
+
+fix(fuselage): `MultiSelect` dropdown reopening when its toggle is clicked while open
diff --git a/packages/fuselage/src/components/MultiSelect/MultiSelect.spec.tsx b/packages/fuselage/src/components/MultiSelect/MultiSelect.spec.tsx
index 2bc3cd7261..0af6dac382 100644
--- a/packages/fuselage/src/components/MultiSelect/MultiSelect.spec.tsx
+++ b/packages/fuselage/src/components/MultiSelect/MultiSelect.spec.tsx
@@ -1,5 +1,5 @@
import { composeStories } from '@storybook/react-webpack5';
-import { screen } from '@testing-library/react';
+import { act, fireEvent, screen } from '@testing-library/react';
import { axe } from 'jest-axe';
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';
@@ -32,6 +32,44 @@ test.each(testCases)(
},
);
+test('stays closed when the toggle is clicked while open (regression for #1869)', () => {
+ // The visibility state is debounced (10ms). When the open dropdown is
+ // clicked, the anchor blurs first (`onBlur: hide`) and that debounced hide
+ // flushes before the trailing `click` fires. Previously `handleClick` read
+ // the already-HIDDEN `visible` state and re-opened the dropdown. Reproduce
+ // that exact ordering with controlled timers.
+ jest.useFakeTimers();
+
+ try {
+ const { Default } = composeStories(stories);
+ render();
+
+ const combobox = screen.getByRole('combobox');
+ const container = combobox.closest('.rcx-select') as HTMLElement;
+
+ // Open the dropdown.
+ fireEvent.click(container);
+ act(() => {
+ jest.advanceTimersByTime(10);
+ });
+ expect(combobox).toHaveAttribute('aria-expanded', 'true');
+
+ // Clicking the toggle: blur closes it, then the click must keep it closed.
+ fireEvent.blur(combobox);
+ act(() => {
+ jest.advanceTimersByTime(10);
+ });
+ fireEvent.click(container);
+ act(() => {
+ jest.advanceTimersByTime(10);
+ });
+
+ expect(combobox).toHaveAttribute('aria-expanded', 'false');
+ } finally {
+ jest.useRealTimers();
+ }
+});
+
test('MultiSelectFiltered prevents Chrome autocomplete overlay (regression)', () => {
const { WithFilter } = composeStories(stories);
diff --git a/packages/fuselage/src/components/MultiSelect/MultiSelect.tsx b/packages/fuselage/src/components/MultiSelect/MultiSelect.tsx
index aa429d01ca..023ffb240c 100644
--- a/packages/fuselage/src/components/MultiSelect/MultiSelect.tsx
+++ b/packages/fuselage/src/components/MultiSelect/MultiSelect.tsx
@@ -142,9 +142,16 @@ const MultiSelect = forwardRef(
};
const handleClick = useStableCallback(() => {
- if (visible === AnimatedVisibility.VISIBLE) {
+ // Toggle based on the self-managed `focus-visible` class rather than the
+ // `visible` state. The anchor's `onBlur` fires `hide()` during the
+ // mousedown that precedes this click, so reading `visible` here would
+ // already see HIDDEN and re-open the dropdown. This mirrors SelectLegacy.
+ if (innerRef.current?.classList.contains('focus-visible')) {
+ removeFocusClass();
return hide();
}
+
+ innerRef.current?.classList.add('focus-visible');
innerRef.current?.focus();
return show();
});