Repository: mapbox/mr-ui
Pull request #236 (2025-04-17 11:11:00)

Studio-3468 Fix clickOutsideCloses property in Popover component
When passing clickOutsideCloses={false} to popover an error was triggered when trying to close the popup or when clicking somewhere outside. This PR fixes this error. 

![image](https://github.com/user-attachments/assets/bd280026-fa14-4929-a3a4-be259e8d91df)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47c702e..0eb1403 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## Changelog
 
+## 2.17.1
+- [fix] Fixes clickOutsideCloses property in Popover component
+
 ## 2.17.0
 
 - [feature] Add a new component `Drawer`
diff --git a/package-lock.json b/package-lock.json
index 1168865..49ba3c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@mapbox/mr-ui",
-  "version": "2.17.0",
+  "version": "2.17.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@mapbox/mr-ui",
-      "version": "2.17.0",
+      "version": "2.17.1",
       "license": "BSD-2-Clause",
       "dependencies": {
         "@mapbox/mbx-assembly": "^1.6.0",
diff --git a/package.json b/package.json
index 88395eb..52ef4e0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@mapbox/mr-ui",
-  "version": "2.17.0",
+  "version": "2.17.1",
   "description": "UI components for Mapbox projects",
   "main": "index.js",
   "homepage": "./",
diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx
index f15e7cf..489c1f9 100644
--- a/src/components/popover/popover.tsx
+++ b/src/components/popover/popover.tsx
@@ -74,6 +74,9 @@ export default function Popover({
   };
 
   const onDown = (e: Event) => {
+    if (!clickOutsideCloses) {
+      return;
+    }
     // Don't call onExit if it wasn't provided.
     if (!onExit) {
       return;
@@ -105,7 +108,7 @@ export default function Popover({
           className={bodyClasses}
           style={bodyStyle}
           onEscapeKeyDown={escapeCloses && onExit}
-          onPointerDownOutside={clickOutsideCloses && onDown}
+          onPointerDownOutside={onDown}
           onOpenAutoFocus={getInitialFocus}
           align={alignment}
           side={placement}


Test patch:
diff --git a/src/components/control-text/__snapshots__/control-text.test.tsx.snap b/src/components/control-text/__snapshots__/control-text.test.tsx.snap
index beed9a4..6e25836 100644
--- a/src/components/control-text/__snapshots__/control-text.test.tsx.snap
+++ b/src/components/control-text/__snapshots__/control-text.test.tsx.snap
@@ -96,7 +96,7 @@ exports[`ControlText all options renders as expected 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(0, -200%); min-width: max-content;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(0, -200%); min-width: max-content; z-index: auto;"
   >
     <div
       class="bg-gray-dark border--gray-dark color-white border shadow-darken25 round px12 py6 border border-1 border--border--gray-dark"
@@ -105,7 +105,7 @@ exports[`ControlText all options renders as expected 1`] = `
       data-state="open"
       id="radix-:r5:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height); animation: none;"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height); animation: none;"
       tabindex="-1"
     >
       oh no!
diff --git a/src/components/popover/__snapshots__/popover.test.tsx.snap b/src/components/popover/__snapshots__/popover.test.tsx.snap
index fb6f063..98d4a4b 100644
--- a/src/components/popover/__snapshots__/popover.test.tsx.snap
+++ b/src/components/popover/__snapshots__/popover.test.tsx.snap
@@ -12,7 +12,7 @@ exports[`Popover all options renders 1`] = `
       style="display: contents;"
     >
       <button
-        aria-controls="radix-:r4:"
+        aria-controls="radix-:r6:"
         aria-expanded="true"
         aria-haspopup="dialog"
         data-state="open"
@@ -25,7 +25,7 @@ exports[`Popover all options renders 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(-6px, 0px); min-width: max-content; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 50%;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(-6px, 0px); min-width: max-content; z-index: auto; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 50%;"
   >
     <div
       class="bg-white border--white color-text border shadow-darken25 round border border-1 border--border--white"
@@ -33,9 +33,9 @@ exports[`Popover all options renders 1`] = `
       data-side="left"
       data-state="open"
       data-testid="foo"
-      id="radix-:r4:"
+      id="radix-:r6:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height); opacity: 0;"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height); opacity: 0;"
       tabindex="-1"
     >
       <span
@@ -83,7 +83,7 @@ exports[`Popover basic renders 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; z-index: auto; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
   >
     <div
       class="bg-white border--white color-text border shadow-darken25 round px12 py12 border border-1 border--border--white"
@@ -92,7 +92,7 @@ exports[`Popover basic renders 1`] = `
       data-state="open"
       id="radix-:r0:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
       tabindex="-1"
     >
       Popup content
@@ -148,7 +148,7 @@ exports[`Popover dark renders 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; z-index: auto; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
   >
     <div
       class="bg-gray-dark border--gray-dark color-white border shadow-darken25 round px12 py12 border border-1 border--border--gray-dark"
@@ -157,7 +157,7 @@ exports[`Popover dark renders 1`] = `
       data-state="open"
       id="radix-:r1:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
       tabindex="-1"
     >
       Content is rendered
@@ -213,7 +213,7 @@ exports[`Popover no arrow renders 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 50%;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; z-index: auto; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 50%;"
   >
     <div
       class="bg-white border--white color-text border shadow-darken25 round px12 py12 border border-1 border--border--white"
@@ -222,7 +222,7 @@ exports[`Popover no arrow renders 1`] = `
       data-state="open"
       id="radix-:r3:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
       tabindex="-1"
     >
       <div>
@@ -263,7 +263,7 @@ exports[`Popover warning renders 1`] = `
   </div>
   <div
     data-radix-popper-content-wrapper=""
-    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
+    style="position: fixed; left: 0px; top: 0px; transform: translate(6px, 0px); min-width: max-content; z-index: auto; --radix-popper-available-width: -6px; --radix-popper-available-height: 0px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0px 0px;"
   >
     <div
       class="bg-orange-faint border--orange-deep color-orange-deep border shadow-darken25 round px12 py12 border border-1 border--border--orange-deep"
@@ -272,7 +272,7 @@ exports[`Popover warning renders 1`] = `
       data-state="open"
       id="radix-:r2:"
       role="dialog"
-      style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
+      style="z-index: auto; --radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
       tabindex="-1"
     >
       <span>
diff --git a/src/components/popover/popover.test.tsx b/src/components/popover/popover.test.tsx
index ebab1af..dfc3565 100644
--- a/src/components/popover/popover.test.tsx
+++ b/src/components/popover/popover.test.tsx
@@ -6,11 +6,13 @@ import userEvent from '@testing-library/user-event';
 const PopoverTest = (props) => {
   const [active, setActive] = useState(false);
   return (
-    <Popover active={active} {...props}>
-      <button data-testid="trigger" onClick={() => setActive(true)}>trigger</button>
+    <Popover onExit={() => setActive(false)} active={active} {...props}>
+      <button data-testid="trigger" onClick={() => setActive(!active)}>
+        trigger
+      </button>
     </Popover>
-  )
-}
+  );
+};
 
 describe('Popover', () => {
   const props = {
@@ -21,7 +23,7 @@ describe('Popover', () => {
     test('renders', async () => {
       const user = userEvent.setup();
       const { baseElement } = render(<PopoverTest {...props} />);
-      await user.click(screen.getByTestId("trigger"));
+      await user.click(screen.getByTestId('trigger'));
       await waitFor(() => {
         expect(screen.getByRole('dialog')).toBeInTheDocument();
       });
@@ -31,7 +33,7 @@ describe('Popover', () => {
 
   describe('dark', () => {
     function renderContent() {
-      return 'Content is rendered'
+      return 'Content is rendered';
     }
 
     const props = {
@@ -42,7 +44,7 @@ describe('Popover', () => {
     test('renders', async () => {
       const user = userEvent.setup();
       const { baseElement } = render(<PopoverTest {...props} />);
-      await user.click(screen.getByTestId("trigger"));
+      await user.click(screen.getByTestId('trigger'));
       await waitFor(() => {
         expect(screen.getByRole('dialog')).toBeInTheDocument();
       });
@@ -59,7 +61,7 @@ describe('Popover', () => {
     test('renders', async () => {
       const user = userEvent.setup();
       const { baseElement } = render(<PopoverTest {...props} />);
-      await user.click(screen.getByTestId("trigger"));
+      await user.click(screen.getByTestId('trigger'));
       await waitFor(() => {
         expect(screen.getByRole('dialog')).toBeInTheDocument();
       });
@@ -76,7 +78,7 @@ describe('Popover', () => {
     test('renders', async () => {
       const user = userEvent.setup();
       const { baseElement } = render(<PopoverTest {...props} />);
-      await user.click(screen.getByTestId("trigger"));
+      await user.click(screen.getByTestId('trigger'));
       await waitFor(() => {
         expect(screen.getByRole('dialog')).toBeInTheDocument();
       });
@@ -84,6 +86,54 @@ describe('Popover', () => {
     });
   });
 
+  describe('clickOutsideCloses', () => {
+    test('does not close when clickOutsideCloses is false and user clicks somewhere else', async () => {
+      const props = {
+        clickOutsideCloses: false
+      };
+
+      const user = userEvent.setup();
+      render(
+        <div>
+          <button data-testid="other">Click me</button>
+          <PopoverTest {...props} />
+        </div>
+      );
+      await user.click(screen.getByTestId('trigger'));
+      await waitFor(() => {
+        expect(screen.getByRole('dialog')).toBeInTheDocument();
+      });
+
+      await user.click(screen.getByTestId('other'));
+      await waitFor(() => {
+        expect(screen.getByRole('dialog')).toBeInTheDocument();
+      });
+    });
+
+    test('closes when clickOutsideCloses is true and user clicks somewhere else', async () => {
+      const props = {
+        clickOutsideCloses: true
+      };
+
+      const user = userEvent.setup();
+      render(
+        <div>
+          <button data-testid="other">Click me</button>
+          <PopoverTest {...props} />
+        </div>
+      );
+      await user.click(screen.getByTestId('trigger'));
+      await waitFor(() => {
+        expect(screen.getByRole('dialog')).toBeInTheDocument();
+      });
+
+      await user.click(screen.getByTestId('other'));
+      await waitFor(() => {
+        expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+      });
+    });
+  });
+
   describe('all options', () => {
     const props = {
       placement: 'left',
@@ -109,7 +159,7 @@ describe('Popover', () => {
     test('renders', async () => {
       const user = userEvent.setup();
       const { baseElement } = render(<PopoverTest {...props} />);
-      await user.click(screen.getByTestId("trigger"));
+      await user.click(screen.getByTestId('trigger'));
       await waitFor(() => {
         expect(screen.getByRole('dialog')).toBeInTheDocument();
       });