Close the login dropdown only on a genuine outside click
Previously any message reaching the widget closed the Sign In dropdown, because the handler returned early only for a clickOutside payload while closing was disabled and fell through to closing in every other case. The embedding page posts hash, title and theme messages of its own, and `embed.ts` installs a MutationObserver on the host page title that posts on every mutation, so a host page whose title changes closes an open login form and discards whatever was typed into it. Browser extensions that post into the page have the same effect. After this change the dropdown closes only for a clickOutside payload from `window.parent`. Reproduced against the deployed demo: with the form open and filled, a single `document.title` assignment on the host page removed it, while three seconds of inactivity did not. Resolves #2139.
This commit is contained in:
committed by
Umputun
parent
5b37a583ce
commit
8bcfd9e456
@@ -23,9 +23,16 @@ export function useDropdown(disableClosing?: boolean) {
|
||||
}
|
||||
|
||||
function handleMessageFromParent(evt: MessageEvent) {
|
||||
// only the embedding page may close the dropdown, and only by reporting a click
|
||||
// outside the widget. the parent also posts hash, title and theme messages, and
|
||||
// extensions post their own, none of which should interrupt an active login
|
||||
if (evt.source !== window.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseMessage(evt);
|
||||
|
||||
if (disableClosing && data.clickOutside) {
|
||||
if (!data.clickOutside || disableClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ describe('<Auth/>', () => {
|
||||
StaticStore.config.auth_providers = defaultProviders;
|
||||
});
|
||||
|
||||
// the embedding page posts into the widget with iframe.contentWindow.postMessage, which
|
||||
// arrives with source set to the parent. jsdom leaves source unset on window.postMessage,
|
||||
// so it is set here explicitly
|
||||
function postFromParent(data: unknown) {
|
||||
window.dispatchEvent(new MessageEvent('message', { data, source: window.parent }));
|
||||
}
|
||||
|
||||
// TODO: separate tests of `useDropdown` mechanics with the hook
|
||||
describe('useDropdown', () => {
|
||||
it('should render auth with hidden dropdown', () => {
|
||||
@@ -54,7 +61,7 @@ describe('<Auth/>', () => {
|
||||
expect(container.querySelector('.auth-dropdown')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close dropdown by message from parent', async () => {
|
||||
it('should close dropdown by clickOutside message from parent', async () => {
|
||||
const { container } = render(<Auth />);
|
||||
|
||||
expect(container.querySelector('.auth-dropdown')).not.toBeInTheDocument();
|
||||
@@ -62,9 +69,38 @@ describe('<Auth/>', () => {
|
||||
fireEvent.click(screen.getByText('Sign In'));
|
||||
expect(container.querySelector('.auth-dropdown')).toBeInTheDocument();
|
||||
|
||||
window.postMessage('{"clickOutside": true}', '*');
|
||||
postFromParent({ clickOutside: true });
|
||||
await waitFor(() => expect(container.querySelector('.auth-dropdown')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it.each([
|
||||
['title', { title: 'New title' }],
|
||||
['hash', { hash: '#comment-1' }],
|
||||
['theme', { theme: 'dark' }],
|
||||
['an unparsable payload', 'clickOutside'],
|
||||
])('should not close dropdown by %s message from parent', async (_, message) => {
|
||||
const { container } = render(<Auth />);
|
||||
|
||||
fireEvent.click(screen.getByText('Sign In'));
|
||||
expect(container.querySelector('.auth-dropdown')).toBeInTheDocument();
|
||||
|
||||
postFromParent(message);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(container.querySelector('.auth-dropdown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not close dropdown by clickOutside message from a foreign source', async () => {
|
||||
const { container } = render(<Auth />);
|
||||
|
||||
fireEvent.click(screen.getByText('Sign In'));
|
||||
expect(container.querySelector('.auth-dropdown')).toBeInTheDocument();
|
||||
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { clickOutside: true }, source: null }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(container.querySelector('.auth-dropdown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user