feat(navigation-menu): enhance context handling and lifecycle management
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user