Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
08bffa9
feat(core): add shared floating tree foundation
pimenovoleg Jun 14, 2026
0246f97
fix: tri-state, DI and etc
pimenovoleg Jun 14, 2026
f9db173
fix: tri-state, DI and etc
pimenovoleg Jun 14, 2026
31d861b
feat(dismissable-layer): parallel Base UI-aligned dismissal engine
pimenovoleg Jun 15, 2026
1aa9f94
feat(focus-scope): owner-Document rework + portal-focus bridge toolkit
pimenovoleg Jun 15, 2026
f065025
feat(floating-focus-manager): rdxFloatingFocusManager skeleton
pimenovoleg Jun 15, 2026
6a7c653
feat(floating-focus-manager): markOthers aria-hidden + marker passes
pimenovoleg Jun 15, 2026
506b35b
feat(floating-focus-manager): close-on-focus-out reading the shared tree
pimenovoleg Jun 15, 2026
d7e2fed
feat(floating-focus-manager): initial-focus orchestration + interacti…
pimenovoleg Jun 15, 2026
aebcf73
feat(focus-scope): tab-order navigation helpers for the portal-focus …
pimenovoleg Jun 15, 2026
38e151f
feat(dialog): provide floating tree + root context — migration ground…
pimenovoleg Jun 15, 2026
18bf891
chore: upd wip commit
pimenovoleg Jun 15, 2026
70e085d
fix(floating): markOthers keeps all owned floating roots, not just th…
pimenovoleg Jun 15, 2026
3d924d5
feat(dialog): migrate to the new floating dismissal + focus engine
pimenovoleg Jun 15, 2026
10eea66
feat(floating): migrate tooltip & preview-card, align dialog with Bas…
pimenovoleg Jun 15, 2026
b7ffad5
chore: upd skills
pimenovoleg Jun 15, 2026
1d00d5b
fix(menu): after refactoring
pimenovoleg Jun 15, 2026
37346b4
chore: added touch sloppy-mode to RdxDismissableCapability
pimenovoleg Jun 15, 2026
12e558d
chore: upd menu
pimenovoleg Jun 15, 2026
bb23b65
feat(select, combobox): migrate to the new floating dismissal engine
pimenovoleg Jun 15, 2026
dfb7a82
chore: update autocomplete and menu
pimenovoleg Jun 16, 2026
d554e4e
chore: upd skills
pimenovoleg Jun 16, 2026
c4798c1
chore: rename
pimenovoleg Jun 16, 2026
b293d16
chore: upd dialog behavior
pimenovoleg Jun 16, 2026
25b7bc2
feat(floating)!: finish dismissal cutover, remove legacy engine, scro…
pimenovoleg Jun 16, 2026
cd59825
chore: upd skills
pimenovoleg Jun 16, 2026
93b5f95
feat(floating): finish dismissal cutover, scroll-lock parity, and foc…
pimenovoleg Jun 16, 2026
cb1dd5f
chore: upd skills
pimenovoleg Jun 16, 2026
89532da
fix: floating marker and menu focus manager parity
pimenovoleg Jun 16, 2026
2c10ba6
fix: modal, popover and dialog
pimenovoleg Jun 16, 2026
f1f779c
chore: added example
pimenovoleg Jun 16, 2026
818010d
chore: upd navigation menu
pimenovoleg Jun 16, 2026
b6193fc
fix: a11y for menu
pimenovoleg Jun 16, 2026
5b2f879
chore: fixed RTL
pimenovoleg Jun 16, 2026
05f6fd4
fix: build
pimenovoleg Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions apps/visual-regression/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { defineConfig, devices } from '@playwright/test';
* baselines can coexist — regenerate the Linux set in the official Playwright Docker image when
* wiring CI.
*/
const PORT = 4400;
const BASE_URL = `http://localhost:${PORT}`;
const PORT = Number(process.env['PLAYWRIGHT_PORT'] ?? 4400);
const BASE_URL = process.env['PLAYWRIGHT_BASE_URL'] ?? `http://localhost:${PORT}`;

export default defineConfig({
testDir: './tests',
Expand Down Expand Up @@ -38,10 +38,12 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: `pnpm exec http-server ../../dist/radix-storybook -p ${PORT} -s -c-1`,
url: `${BASE_URL}/index.json`,
reuseExistingServer: !process.env.CI,
timeout: 120_000
}
webServer: process.env['PLAYWRIGHT_NO_WEBSERVER']
? undefined
: {
command: `pnpm exec http-server ../../dist/radix-storybook -p ${PORT} -s -c-1`,
url: `${BASE_URL}/index.json`,
reuseExistingServer: !process.env.CI,
timeout: 120_000
}
});
36 changes: 36 additions & 0 deletions apps/visual-regression/tests/autocomplete.behavior.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@ const visibleItems = '[rdxAutocompleteItem]:not([hidden])';
const highlighted = '[rdxAutocompleteItem][data-highlighted]';
const popup = '[rdxAutocompletePopup]';

/**
* ADR 0015/0017 Phase-4 migration of Autocomplete onto the new floating dismissal engine.
*/
test.describe('Autocomplete — new floating engine migration', () => {
test('Escape closes the autocomplete', async ({ page }) => {
await gotoStory(page, 'primitives-autocomplete--default');
await page.locator(input).click();
await page.locator(input).pressSequentially('f');
await expect(page.locator(popup)).toBeVisible();

await page.keyboard.press('Escape');
await expect(page.locator(popup)).toHaveCount(0);
});

test('an outside press closes the autocomplete', async ({ page }) => {
await gotoStory(page, 'primitives-autocomplete--default');
await page.locator(input).click();
await page.locator(input).pressSequentially('f');
await expect(page.locator(popup)).toBeVisible();

await page.mouse.click(5, 5);
await expect(page.locator(popup)).toHaveCount(0);
});

test('a press on the input keeps the popup open (the input is registered inside)', async ({ page }) => {
await gotoStory(page, 'primitives-autocomplete--default');
await page.locator(input).click();
await page.locator(input).pressSequentially('f');
await expect(page.locator(popup)).toBeVisible();

// Clicking the input again must not self-dismiss — it is "inside" the floating layer.
await page.locator(input).click();
await expect(page.locator(popup)).toBeVisible();
});
});

test.describe('Autocomplete auto highlight', () => {
test('typing a full match highlights the item so Enter selects it', async ({ page }) => {
await gotoStory(page, 'primitives-autocomplete--auto-highlight');
Expand Down
26 changes: 26 additions & 0 deletions apps/visual-regression/tests/combobox.behavior.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ test('autocomplete teleports the positioner directly into <body> with no wrapper
expect(parentTag).toBe('BODY');
});

/**
* ADR 0015/0017 Phase-4 migration of Combobox onto the new floating dismissal engine.
*/
test.describe('Combobox — new floating engine migration', () => {
const input = '[rdxComboboxInput]';
const popup = '[rdxComboboxPopup]';

test('Escape closes the combobox', async ({ page }) => {
await gotoStory(page, 'primitives-combobox--default');
await page.locator(input).click();
await expect(page.locator(popup)).toBeVisible();

await page.keyboard.press('Escape');
await expect(page.locator(popup)).toHaveCount(0);
});

test('an outside press closes the combobox', async ({ page }) => {
await gotoStory(page, 'primitives-combobox--default');
await page.locator(input).click();
await expect(page.locator(popup)).toBeVisible();

await page.mouse.click(5, 5);
await expect(page.locator(popup)).toHaveCount(0);
});
});

/**
* Regression: in multiple mode, once focus has stepped into the chips (ArrowLeft from the input),
* ArrowDown / ArrowUp must hand focus back to the input and engage the list — otherwise the popup
Expand Down
67 changes: 67 additions & 0 deletions apps/visual-regression/tests/context-menu.behavior.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, Page, test } from '@playwright/test';

/**
* ADR 0015/0017 Phase-4 migration of Context Menu (composes `RdxMenuRoot`, so it inherits the new
* floating dismissal engine) onto a real browser. Context menus open at the cursor via a virtual
* anchor; these guard that opening + every dismissal path still works and throws no runtime errors.
*/
async function gotoStory(page: Page, storyId: string): Promise<void> {
await page.goto(`/iframe.html?id=${storyId}&viewMode=story`);
await page.waitForSelector('#storybook-root', { state: 'attached' });
}

const trigger = '[rdxContextMenuTrigger]';
const popup = '[rdxMenuPopup]';

async function openAtTrigger(page: Page): Promise<void> {
await page.locator(trigger).first().click({ button: 'right' });
await expect(page.locator(popup)).toBeVisible();
}

test('right-click opens the context menu without runtime errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(String(e)));
await gotoStory(page, 'primitives-context-menu--default');

await openAtTrigger(page);
expect(errors).toEqual([]);
});

test('Escape closes the context menu', async ({ page }) => {
await gotoStory(page, 'primitives-context-menu--default');
await openAtTrigger(page);

await page.keyboard.press('Escape');
await expect(page.locator(popup)).toHaveCount(0);
});

test('a modal context menu renders an internal backdrop (finding #1)', async ({ page }) => {
await gotoStory(page, 'primitives-context-menu--default');
await openAtTrigger(page);

await expect(page.locator('[data-rdx-menu-internal-backdrop]')).toHaveCount(1);
});

test('a modal context menu traps focus — a focus-out does not close it (finding #3)', async ({ page }) => {
await gotoStory(page, 'primitives-context-menu--default');
await openAtTrigger(page);

// Programmatically move focus to an element outside the menu. A context menu is the one menu kind
// that TRAPS focus (Base UI `FloatingFocusManager modal`), so focus is pulled back and it stays open.
await page.evaluate(() => {
const b = document.createElement('button');
b.id = 'cm-outside';
document.body.appendChild(b);
b.focus();
});
await page.waitForTimeout(120); // let the async focus-out check settle
await expect(page.locator(popup)).toBeVisible();
});

test('an outside press closes the context menu', async ({ page }) => {
await gotoStory(page, 'primitives-context-menu--default');
await openAtTrigger(page);

await page.mouse.click(5, 5);
await expect(page.locator(popup)).toHaveCount(0);
});
Loading
Loading