feat(navigation-menu): enhance context handling and lifecycle management

This commit is contained in:
2026-06-10 16:16:12 +07:00
parent a82f5f2dfd
commit 9375304e1a
55 changed files with 1997 additions and 179 deletions
@@ -40,6 +40,11 @@ function press(el: Element, key: string) {
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
}
// EditableRoot defers its outside-blur decision by a macrotask.
function waitForBlurTimers() {
return new Promise(resolve => setTimeout(resolve, 20));
}
describe('Editable', () => {
it('renders preview with default placeholder when empty', () => {
const w = createEditable({ placeholder: 'Click to edit' });
@@ -169,4 +174,101 @@ describe('Editable', () => {
expect((w.find('input').element as HTMLInputElement).hidden).toBe(true);
w.unmount();
});
it('keeps edit mode when the focused edit trigger hides itself (real focus)', async () => {
const w = createEditable({ defaultValue: 'v', activationMode: 'none' });
const editBtn = w.findAll('button').find(b => b.attributes('aria-label') === 'edit')!;
(editBtn.element as HTMLButtonElement).focus();
(editBtn.element as HTMLButtonElement).click();
await nextTick();
await waitForBlurTimers();
expect((w.find('input').element as HTMLInputElement).hidden).toBe(false);
expect(document.activeElement).toBe(w.find('input').element);
expect(w.findComponent(EditableRoot).emitted('update:state')?.flat()).toEqual(['edit']);
w.unmount();
});
it('keeps edit mode when the really-focused preview hides itself (focus activation)', async () => {
const w = createEditable({ defaultValue: 'v', activationMode: 'focus' });
(w.find('span').element as HTMLElement).focus();
await nextTick();
await waitForBlurTimers();
expect((w.find('input').element as HTMLInputElement).hidden).toBe(false);
expect(document.activeElement).toBe(w.find('input').element);
expect(w.findComponent(EditableRoot).emitted('update:state')?.flat()).toEqual(['edit']);
w.unmount();
});
it('still submits when focus genuinely leaves the root (submitMode blur)', async () => {
const outside = document.createElement('button');
document.body.appendChild(outside);
const w = createEditable({ defaultValue: 'v1', startWithEditMode: true, submitMode: 'blur' });
await nextTick();
const input = w.find('input');
(input.element as HTMLInputElement).focus();
(input.element as HTMLInputElement).value = 'v2';
await input.trigger('input');
outside.focus();
await waitForBlurTimers();
const root = w.findComponent(EditableRoot);
expect(root.emitted('submit')?.at(-1)).toEqual(['v2']);
expect((w.find('input').element as HTMLInputElement).hidden).toBe(true);
outside.remove();
w.unmount();
});
it('hides parts even when consumer display utility classes override [hidden]', async () => {
const style = document.createElement('style');
style.textContent = '.u-block { display: block; } .u-inline-flex { display: inline-flex; }';
document.head.appendChild(style);
const w = mount(
defineComponent({
setup() {
return () => h(
EditableRoot,
{ defaultValue: 'v', activationMode: 'none' },
{
default: () => h(EditableArea, null, {
default: () => [
h(EditablePreview, { class: 'u-block' }),
h(EditableInput, { class: 'u-block' }),
h(EditableEditTrigger, { class: 'u-inline-flex' }),
h(EditableSubmitTrigger, { class: 'u-inline-flex' }),
h(EditableCancelTrigger, { class: 'u-inline-flex' }),
],
}),
},
);
},
}),
{ attachTo: document.body },
);
const button = (label: string) =>
w.findAll('button').find(b => b.attributes('aria-label') === label)!.element as HTMLElement;
expect(getComputedStyle(w.find('input').element).display).toBe('none');
expect(getComputedStyle(button('submit')).display).toBe('none');
expect(getComputedStyle(button('cancel')).display).toBe('none');
expect(getComputedStyle(button('edit')).display).toBe('inline-flex');
await w.findAll('button').find(b => b.attributes('aria-label') === 'edit')!.trigger('click');
await nextTick();
expect(getComputedStyle(w.find('span').element).display).toBe('none');
expect(getComputedStyle(button('edit')).display).toBe('none');
expect(getComputedStyle(w.find('input').element).display).toBe('block');
expect(getComputedStyle(button('submit')).display).toBe('inline-flex');
expect(getComputedStyle(button('cancel')).display).toBe('inline-flex');
style.remove();
w.unmount();
});
it('submit and cancel triggers are no-ops while not editing', async () => {
const w = createEditable({ defaultValue: 'v1' });
const submitBtn = w.findAll('button').find(b => b.attributes('aria-label') === 'submit')!;
const cancelBtn = w.findAll('button').find(b => b.attributes('aria-label') === 'cancel')!;
await submitBtn.trigger('click');
await cancelBtn.trigger('click');
const root = w.findComponent(EditableRoot);
expect(root.emitted('submit')).toBeUndefined();
expect(root.emitted('update:state')).toBeUndefined();
w.unmount();
});
});