mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
fix: close a plugin dialog on Escape while the focus is in one of its fields
This commit is contained in:
@@ -136,6 +136,34 @@ describe('slot sizing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Escape inside a slot frame', () => {
|
||||
const inFrame = (wrapperAttr?: string) => {
|
||||
const wrapper = document.createElement('div');
|
||||
if (wrapperAttr) wrapper.setAttribute(wrapperAttr, 'p1');
|
||||
document.body.appendChild(wrapper);
|
||||
const inst = createSlotInstance({
|
||||
plugin: plugin(), slot: 'plugin-dialog', code: '', locale: 'en', tier: 'untrusted', extraProps: {}, hostContainer: wrapper,
|
||||
});
|
||||
instances.push(inst);
|
||||
const keys: string[] = [];
|
||||
const onKey = (e: KeyboardEvent) => { keys.push(e.key); };
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
post(inst.iframe.contentWindow, 'null', { type: 'frame-escape' });
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
wrapper.remove();
|
||||
return keys;
|
||||
};
|
||||
|
||||
it('reaches the host dialog when the frame is one', () => {
|
||||
expect(inFrame('data-plugin-dialog')).toEqual(['Escape']);
|
||||
expect(inFrame('data-plugin-dialog-popover')).toEqual(['Escape']);
|
||||
});
|
||||
|
||||
it('stays in the frame anywhere else', () => {
|
||||
expect(inFrame()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShow context', () => {
|
||||
it('leaves host callbacks out instead of failing to post them', async () => {
|
||||
const inst = booted(createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' }));
|
||||
|
||||
@@ -551,6 +551,15 @@ export class SandboxInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
case 'frame-escape': {
|
||||
// Only for a dialog's own frame: elsewhere Escape in a frame means
|
||||
// nothing to the host, and shouldn't close the message around it.
|
||||
if (this.iframe.closest('[data-plugin-dialog], [data-plugin-dialog-popover]')) {
|
||||
this.iframe.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'slot-resize': {
|
||||
// The iframe has no intrinsic height - sync it to the content height
|
||||
// the sandbox reported, otherwise the wrapper reserves space but the
|
||||
|
||||
@@ -179,6 +179,12 @@ export interface SlotResizeMsg {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape pressed inside a slot frame and left alone by the plugin. Keys in a
|
||||
* frame never reach the host, so a plugin dialog would not close on it.
|
||||
*/
|
||||
export interface FrameEscapeMsg { type: 'frame-escape'; }
|
||||
|
||||
export interface SlotShouldShowResultMsg {
|
||||
type: 'slot-should-show-result';
|
||||
id: string;
|
||||
@@ -235,6 +241,7 @@ export type SandboxToHost =
|
||||
| CallbackInvokeMsg
|
||||
| HookResultMsg
|
||||
| SlotResizeMsg
|
||||
| FrameEscapeMsg
|
||||
| ThemeAppliedMsg
|
||||
| SlotShouldShowResultMsg
|
||||
| FloatingDropdownOpenMsg
|
||||
|
||||
@@ -1516,6 +1516,10 @@ async function bootSlot(payload: SlotInit): Promise<void> {
|
||||
|
||||
const reactRoot = renderer.ReactDOM.createRoot(rootEl);
|
||||
reactRoot.render(React.createElement(SlotShell));
|
||||
// Last in line: a dropdown in the frame that closes on Escape prevents the default.
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && event.isTrusted && !event.defaultPrevented) sendToHost({ type: 'frame-escape' });
|
||||
});
|
||||
sendToHost({ type: 'init-done', hooks: [], slots: [], shortcuts: [] });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user