- Force font-size:16px !important on Styles-sheet/topbar/Sitesmith inputs inside the mobile media query so inline 12px/14px styles stop triggering iOS zoom-on-focus. - Lift sheet-open + Templates/Head Code modal-open state out of private useState into a shared MobileChromeContext (EditorShell), so Phase B can open/close sheets from outside MobilePanelBar. - Add an explicit z-index layer scale, portal TemplateModal to document.body (was trapped under the tab bar inside .topbar's stacking context), align Sitesmith to the same --z-modal layer, and make opening a sheet close any open modal. Also fix modal backdrops swallowing tab bar taps (mirrors the sheet backdrop's existing tab-bar cutout). - Drop BottomSheet's incorrect aria-modal; mobile-aware AssetsPanel empty state copy. - Tests: useIsMobile (matchMedia mock incl. legacy fallback + cleanup), MobileChromeContext invariants (one sheet open, sheet closes modals), MobilePanelBar wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
import { describe, test, expect, vi } from 'vitest';
|
|
import React from 'react';
|
|
import { createRoot, Root } from 'react-dom/client';
|
|
import { act } from 'react-dom/test-utils';
|
|
import { MobilePanelBar } from './MobilePanelBar';
|
|
import { MobileChromeProvider } from '../../state/MobileChromeContext';
|
|
|
|
/**
|
|
* Mobile-A2 (review item 5): the real BlocksPanel/PagesPanel/LayersPanel/
|
|
* AssetsPanel/GuidedStyles all pull in Craft.js's `useEditor` and other
|
|
* heavy dependencies not relevant here, so they're stubbed out -- this
|
|
* suite is only exercising MobilePanelBar's OWN wiring: tapping a tab opens
|
|
* that tab's sheet with only its panel rendered, tapping the active tab
|
|
* again closes it, and (mirroring MobileChromeContext.test.tsx) only one
|
|
* sheet's content is ever mounted at once.
|
|
*/
|
|
vi.mock('../left/BlocksPanel', () => ({ BlocksPanel: () => <div data-testid="panel-blocks" /> }));
|
|
vi.mock('../left/PagesPanel', () => ({ PagesPanel: () => <div data-testid="panel-pages" /> }));
|
|
vi.mock('../left/LayersPanel', () => ({ LayersPanel: () => <div data-testid="panel-layers" /> }));
|
|
vi.mock('../left/AssetsPanel', () => ({ AssetsPanel: () => <div data-testid="panel-assets" /> }));
|
|
vi.mock('../right/GuidedStyles', () => ({ GuidedStyles: () => <div data-testid="panel-styles" /> }));
|
|
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
|
|
function render(ui: React.ReactElement) {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
act(() => {
|
|
root = createRoot(container);
|
|
root.render(ui);
|
|
});
|
|
}
|
|
|
|
function unmount() {
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
}
|
|
|
|
function tabButton(label: string): HTMLButtonElement {
|
|
const buttons = Array.from(container.querySelectorAll('.mobile-tab-btn')) as HTMLButtonElement[];
|
|
const btn = buttons.find((b) => b.textContent?.includes(label));
|
|
if (!btn) throw new Error(`No tab button found for "${label}"`);
|
|
return btn;
|
|
}
|
|
|
|
function openPanelIds(): string[] {
|
|
return Array.from(container.querySelectorAll('[data-testid^="panel-"]')).map(
|
|
(el) => (el as HTMLElement).dataset.testid!,
|
|
);
|
|
}
|
|
|
|
describe('MobilePanelBar', () => {
|
|
test('no sheet is open initially', () => {
|
|
render(
|
|
<MobileChromeProvider>
|
|
<MobilePanelBar />
|
|
</MobileChromeProvider>,
|
|
);
|
|
expect(container.querySelector('.mobile-sheet-backdrop')).toBeNull();
|
|
expect(openPanelIds()).toEqual([]);
|
|
unmount();
|
|
});
|
|
|
|
test('tapping a tab opens exactly that tab\'s panel', () => {
|
|
render(
|
|
<MobileChromeProvider>
|
|
<MobilePanelBar />
|
|
</MobileChromeProvider>,
|
|
);
|
|
|
|
act(() => {
|
|
tabButton('Blocks').click();
|
|
});
|
|
expect(openPanelIds()).toEqual(['panel-blocks']);
|
|
|
|
unmount();
|
|
});
|
|
|
|
test('switching tabs replaces the open sheet -- only one panel mounted at a time', () => {
|
|
render(
|
|
<MobileChromeProvider>
|
|
<MobilePanelBar />
|
|
</MobileChromeProvider>,
|
|
);
|
|
|
|
act(() => {
|
|
tabButton('Blocks').click();
|
|
});
|
|
expect(openPanelIds()).toEqual(['panel-blocks']);
|
|
|
|
act(() => {
|
|
tabButton('Styles').click();
|
|
});
|
|
expect(openPanelIds()).toEqual(['panel-styles']);
|
|
|
|
act(() => {
|
|
tabButton('Assets').click();
|
|
});
|
|
expect(openPanelIds()).toEqual(['panel-assets']);
|
|
|
|
unmount();
|
|
});
|
|
|
|
test('tapping the active tab again closes the sheet', () => {
|
|
render(
|
|
<MobileChromeProvider>
|
|
<MobilePanelBar />
|
|
</MobileChromeProvider>,
|
|
);
|
|
|
|
act(() => {
|
|
tabButton('Pages').click();
|
|
});
|
|
expect(openPanelIds()).toEqual(['panel-pages']);
|
|
expect(container.querySelector('.mobile-sheet-backdrop')).not.toBeNull();
|
|
|
|
act(() => {
|
|
tabButton('Pages').click();
|
|
});
|
|
expect(openPanelIds()).toEqual([]);
|
|
expect(container.querySelector('.mobile-sheet-backdrop')).toBeNull();
|
|
|
|
unmount();
|
|
});
|
|
|
|
test('active tab button carries aria-pressed=true only for the open sheet', () => {
|
|
render(
|
|
<MobileChromeProvider>
|
|
<MobilePanelBar />
|
|
</MobileChromeProvider>,
|
|
);
|
|
|
|
act(() => {
|
|
tabButton('Layers').click();
|
|
});
|
|
|
|
expect(tabButton('Layers').getAttribute('aria-pressed')).toBe('true');
|
|
expect(tabButton('Blocks').getAttribute('aria-pressed')).toBe('false');
|
|
|
|
unmount();
|
|
});
|
|
});
|