diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.test.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.test.tsx
index 290ca1ac9c..3239816d49 100644
--- a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.test.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.test.tsx
@@ -16,55 +16,73 @@ beforeAll(() => {
});
const ALLOWED = ['admin', 'auditor', 'employee', 'contractor'];
+const CUSTOM_ROLES = [
+ { id: 'orole_1', name: 'Security Reviewer', permissions: { control: ['read'] } },
+];
-async function openAndGetRoleItem(name: string) {
- const user = userEvent.setup();
- await user.click(screen.getByRole('combobox'));
- const item = (await screen.findByText(name)).closest('[cmdk-item]');
- if (!(item instanceof HTMLElement)) {
- throw new Error(`Role item "${name}" not found`);
- }
- return { user, item };
-}
-
-describe('MultiRoleCombobox selection', () => {
- it('does not cancel pointerdown on a role item (regression: role not selectable)', async () => {
- // The role items previously had `onPointerDown={(e) => e.preventDefault()}`.
- // Cancelling pointerdown suppresses the browser's native pointer->click
- // sequence that drives cmdk's `onSelect`, so clicking a role did nothing.
- // jsdom fires a synthetic click that bypasses the pointer sequence, so we
- // assert the underlying DOM contract directly: the item must let the
- // pointerdown default action proceed.
+describe('MultiRoleCombobox (CS-748)', () => {
+ it('forces pointer-events on the popover content so roles stay clickable inside a modal dialog', async () => {
+ // This combobox is used inside the modal "Add User" Radix Dialog, which
+ // locks `body { pointer-events: none }`. A Radix dismissable-layer version
+ // skew (react-dialog -> 1.1.15 vs react-popover -> 1.1.11) puts the two in
+ // separate module-level layer contexts, so the portaled popover never
+ // re-enables pointer events and its role items inherit `none` — unclickable
+ // and unhoverable. The fix forces pointer-events on the content. jsdom can't
+ // hit-test, so we assert the fix is present here; the end-to-end click
+ // behavior inside the modal is verified in a real browser.
+ const user = userEvent.setup();
render(
,
);
- const { item } = await openAndGetRoleItem('Admin');
- const pointerDown = new PointerEvent('pointerdown', { bubbles: true, cancelable: true });
- item.dispatchEvent(pointerDown);
+ await user.click(screen.getByRole('combobox'));
- expect(pointerDown.defaultPrevented).toBe(false);
+ const content = document.querySelector('[data-slot="popover-content"]');
+ expect(content).not.toBeNull();
+ expect(content).toHaveStyle({ pointerEvents: 'auto' });
});
- it('adds a role to the selection when clicked', async () => {
+ it('selects a built-in role when clicked', async () => {
const handleChange = vi.fn();
+ const user = userEvent.setup();
render(
,
);
- const { user, item } = await openAndGetRoleItem('Admin');
- await user.click(item);
+ await user.click(screen.getByRole('combobox'));
+ await user.click(await screen.findByText('Admin'));
expect(handleChange).toHaveBeenCalledWith(['admin']);
});
+
+ it('selects a custom role when clicked (the customer-reported path)', async () => {
+ const handleChange = vi.fn();
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole('combobox'));
+ await user.click(await screen.findByText('Security Reviewer'));
+
+ expect(handleChange).toHaveBeenCalledWith(['Security Reviewer']);
+ });
});
diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx
index 12e6b38c87..7e2e19619e 100644
--- a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx
@@ -169,7 +169,22 @@ export function MultiRoleCombobox({
/>
-
+ {/*
+ This Popover is portaled to while rendered inside a modal Radix
+ Dialog (the invite "Add User" dialog). The Dialog locks
+ `body { pointer-events: none }`, and a Radix version skew
+ (react-dialog -> react-dismissable-layer@1.1.15 vs
+ react-popover -> react-dismissable-layer@1.1.11) means the two live in
+ separate module-level layer contexts, so the popover never re-enables
+ pointer events on itself and its items inherit `none` — making every
+ role unclickable/unhoverable. Forcing pointer-events here restores
+ interactivity for the whole popover subtree. See CS-748.
+ */}
+