fix(site-builder): nav link href typing is one undo burst again
URL/anchor/tel/mailto edits from LinkPicker now go through continuousSetProp (key link:<i>:href); a page pick stays a discrete step via discreteSetProp. source/pageId stripping and the followPages mergeLinks re-merge remain inside the updater. Refs #53 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52477bff45
commit
4379ffac64
2 files changed
+63
-3
No files matched your search
@@ -13,9 +13,12 @@ const setPropSpy = vi.fn((_id: string, updater: (p: any) => void) => {
|
||||
updater(lastProps);
|
||||
});
|
||||
let lastProps: any;
|
||||
/* history.merge() is a spy so undo-burst behaviour is observable: a call to
|
||||
it means the setProp folded into the previous undo entry. */
|
||||
const mergeSpy = vi.fn(() => ({ setProp: setPropSpy }));
|
||||
|
||||
vi.mock('@craftjs/core', () => ({
|
||||
useEditor: () => ({ actions: { setProp: setPropSpy, history: { merge: () => ({ setProp: setPropSpy }) } } }),
|
||||
useEditor: () => ({ actions: { setProp: setPropSpy, history: { merge: mergeSpy } } }),
|
||||
}));
|
||||
|
||||
let mockPages: { id: string; name: string; slug: string; craftState: string | null; nav?: any }[] = [];
|
||||
@@ -31,6 +34,7 @@ vi.mock('../../../ui/AssetPicker', () => ({
|
||||
}));
|
||||
|
||||
import { NavStylePanel, LinkPicker, pageHref } from './NavStylePanel';
|
||||
import { closeContinuousBurst } from './shared';
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
@@ -70,6 +74,8 @@ function click(el: Element | null) {
|
||||
beforeEach(() => {
|
||||
try { window.localStorage.clear(); } catch { /* ignore */ }
|
||||
setPropSpy.mockClear();
|
||||
mergeSpy.mockClear();
|
||||
closeContinuousBurst();
|
||||
mockPageApi = {};
|
||||
mockPages = [
|
||||
{ id: 'home', name: 'Home', slug: 'index', craftState: null },
|
||||
@@ -373,3 +379,52 @@ describe('NavStylePanel: Follow pages (#53)', () => {
|
||||
expect(lastProps.links[0].children).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NavStylePanel: link href typing is one undo burst (#53 fix round 1)', () => {
|
||||
const typeInto = (el: HTMLInputElement, v: string) => {
|
||||
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!;
|
||||
act(() => { setter.call(el, v); el.dispatchEvent(new Event('input', { bubbles: true })); });
|
||||
};
|
||||
const hrefInput = () => container.querySelector('[data-testid="link-picker"] input[type="text"]') as HTMLInputElement;
|
||||
|
||||
test('consecutive URL keystrokes fold into one history entry via history.merge()', () => {
|
||||
lastProps = { links: [{ text: 'Docs', href: 'https://e' }] };
|
||||
render(<NavStylePanel selectedId="node1" nodeProps={lastProps} />);
|
||||
for (const v of ['https://ex', 'https://exa', 'https://exam', 'https://example.com']) typeInto(hrefInput(), v);
|
||||
expect(setPropSpy).toHaveBeenCalledTimes(4);
|
||||
expect(mergeSpy).toHaveBeenCalledTimes(3); // first keystroke opens the entry, the rest merge
|
||||
expect(lastProps.links[0]).toEqual({ text: 'Docs', href: 'https://example.com' });
|
||||
});
|
||||
|
||||
test('tel typing on a Follow-pages nav merges too, strips source/pageId and keeps the page merge', () => {
|
||||
lastProps = {
|
||||
followPages: true,
|
||||
links: [
|
||||
{ text: 'Home', href: '/', source: 'page', pageId: 'home' },
|
||||
{ text: 'About', href: '/about', source: 'page', pageId: 'about' },
|
||||
{ text: 'Call', href: 'tel:1' },
|
||||
],
|
||||
};
|
||||
render(<NavStylePanel selectedId="node1" nodeProps={lastProps} />);
|
||||
const telInput = Array.from(container.querySelectorAll('[data-testid="link-picker"] input[type="text"]'))
|
||||
.find((el) => (el as HTMLInputElement).placeholder === '+15551234567') as HTMLInputElement;
|
||||
for (const v of ['15', '155', '1555']) typeInto(telInput, v);
|
||||
expect(mergeSpy).toHaveBeenCalledTimes(2);
|
||||
const call = lastProps.links.find((l: any) => l.text === 'Call');
|
||||
expect(call).toEqual({ text: 'Call', href: 'tel:1555' });
|
||||
// page links are still present (mergeLinks ran inside the updater)
|
||||
expect(lastProps.links.filter((l: any) => l.source === 'page').map((l: any) => l.pageId)).toEqual(['home', 'about']);
|
||||
});
|
||||
|
||||
test('a page pick after typing is its own undo step (no merge)', () => {
|
||||
lastProps = { links: [{ text: 'Go', href: 'https://a' }] };
|
||||
render(<NavStylePanel selectedId="node1" nodeProps={lastProps} />);
|
||||
typeInto(hrefInput(), 'https://ab');
|
||||
typeInto(hrefInput(), 'https://abc');
|
||||
expect(mergeSpy).toHaveBeenCalledTimes(1);
|
||||
const modeSelect = container.querySelector('[data-testid="link-picker"] select') as HTMLSelectElement;
|
||||
setValue(modeSelect, 'page');
|
||||
expect(mergeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(lastProps.links[0]).toEqual({ text: 'Go', href: '/', source: 'page', pageId: 'home' });
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
AnimationControl,
|
||||
VisibilityControl,
|
||||
continuousSetProp,
|
||||
discreteSetProp,
|
||||
} from './shared';
|
||||
import { AssetPicker } from '../../../ui/AssetPicker';
|
||||
import { usePages } from '../../../state/PageContext';
|
||||
@@ -204,14 +205,18 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
|
||||
/* #53: LinkPicker reports the picked page; a page pick records
|
||||
source:'page' + pageId, any other mode makes the link manual again. */
|
||||
const updateLinkHref = useCallback((index: number, href: string, pageId: string | null) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updater = (props: any) => {
|
||||
const updated = [...(props.links || [])];
|
||||
const { source: _s, pageId: _p, ...rest } = updated[index] || {};
|
||||
updated[index] = pageId ? { ...rest, href, source: 'page', pageId } : { ...rest, href };
|
||||
// On a page-following nav the write is re-merged, so a link re-pointed
|
||||
// at a page is materialised (and deduped) at once.
|
||||
props.links = props.followPages ? mergeLinks(updated, pages) : updated;
|
||||
});
|
||||
};
|
||||
// A page pick is a discrete choice (its own undo step); URL / anchor /
|
||||
// tel / mailto typing is one undo burst per control, as before #53.
|
||||
if (pageId) discreteSetProp(actions, selectedId, updater);
|
||||
else continuousSetProp(actions, selectedId, `link:${index}:href`, updater);
|
||||
}, [actions, selectedId, pages]);
|
||||
|
||||
/* Nested (dropdown) links of a MANUAL link. Page nesting is set per page in
|
||||
|
||||
Reference in new issue
Block a user