Commit e45b5e0f authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Make host's ToastMessages subscribe to events via emitter instead of sidebar RPC

parent 340e5850
...@@ -2,22 +2,18 @@ import { useCallback, useEffect, useState } from 'preact/hooks'; ...@@ -2,22 +2,18 @@ import { useCallback, useEffect, useState } from 'preact/hooks';
import BaseToastMessages from '../../shared/components/BaseToastMessages'; import BaseToastMessages from '../../shared/components/BaseToastMessages';
import type { ToastMessage } from '../../shared/components/BaseToastMessages'; import type { ToastMessage } from '../../shared/components/BaseToastMessages';
import type { PortRPC } from '../../shared/messaging'; import type { Emitter } from '../util/emitter';
import type {
HostToSidebarEvent,
SidebarToHostEvent,
} from '../../types/port-rpc-events';
export type ToastMessagesProps = { export type ToastMessagesProps = {
sidebarRPC: PortRPC<SidebarToHostEvent, HostToSidebarEvent>; emitter: Emitter;
}; };
/** /**
* A component that renders toast messages coming from the sidebar, in a way * A component that renders toast messages published from the sidebar, in a way
* that they "appear" in the viewport even when the sidebar is collapsed. * that they "appear" in the viewport even when the sidebar is collapsed.
* This is useful to make sure screen readers announce hidden messages. * This is useful to make sure screen readers announce hidden messages.
*/ */
export default function ToastMessages({ sidebarRPC }: ToastMessagesProps) { export default function ToastMessages({ emitter }: ToastMessagesProps) {
const [messages, setMessages] = useState<ToastMessage[]>([]); const [messages, setMessages] = useState<ToastMessage[]>([]);
const addMessage = useCallback( const addMessage = useCallback(
(newMessage: ToastMessage) => setMessages(prev => [...prev, newMessage]), (newMessage: ToastMessage) => setMessages(prev => [...prev, newMessage]),
...@@ -30,9 +26,14 @@ export default function ToastMessages({ sidebarRPC }: ToastMessagesProps) { ...@@ -30,9 +26,14 @@ export default function ToastMessages({ sidebarRPC }: ToastMessagesProps) {
); );
useEffect(() => { useEffect(() => {
sidebarRPC.on('toastMessageAdded', addMessage); emitter.subscribe('toastMessageAdded', addMessage);
sidebarRPC.on('toastMessageDismissed', dismissMessage); emitter.subscribe('toastMessageDismissed', dismissMessage);
}, [sidebarRPC, dismissMessage, addMessage]);
return () => {
emitter.unsubscribe('toastMessageAdded', addMessage);
emitter.unsubscribe('toastMessageDismissed', dismissMessage);
};
}, [emitter, dismissMessage, addMessage]);
return ( return (
<BaseToastMessages messages={messages} onMessageDismiss={dismissMessage} /> <BaseToastMessages messages={messages} onMessageDismiss={dismissMessage} />
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import EventEmitter from 'tiny-emitter'; import EventEmitter from 'tiny-emitter';
import { Emitter } from '../../util/emitter';
import ToastMessages from '../ToastMessages'; import ToastMessages from '../ToastMessages';
describe('ToastMessages', () => { describe('ToastMessages', () => {
let emitter; let emitter;
let fakeSidebarRPC;
const fakeMessage = (id = 'someId') => ({ const fakeMessage = (id = 'someId') => ({
id, id,
...@@ -15,23 +15,21 @@ describe('ToastMessages', () => { ...@@ -15,23 +15,21 @@ describe('ToastMessages', () => {
moreInfoURL: 'http://www.example.com', moreInfoURL: 'http://www.example.com',
}); });
const createComponent = () => const createComponent = () => mount(<ToastMessages emitter={emitter} />);
mount(<ToastMessages sidebarRPC={fakeSidebarRPC} />);
beforeEach(() => { beforeEach(() => {
emitter = new EventEmitter(); emitter = new Emitter(new EventEmitter());
fakeSidebarRPC = { on: (...args) => emitter.on(...args) };
}); });
it('pushes new toast messages on toastMessageAdded', () => { it('adds new toast messages on toastMessageAdded', () => {
const wrapper = createComponent(); const wrapper = createComponent();
// Initially messages is empty // Initially messages is empty
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 0); assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 0);
emitter.emit('toastMessageAdded', fakeMessage('someId1')); emitter.publish('toastMessageAdded', fakeMessage('someId1'));
emitter.emit('toastMessageAdded', fakeMessage('someId2')); emitter.publish('toastMessageAdded', fakeMessage('someId2'));
emitter.emit('toastMessageAdded', fakeMessage('someId3')); emitter.publish('toastMessageAdded', fakeMessage('someId3'));
wrapper.update(); wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 3); assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 3);
...@@ -41,14 +39,14 @@ describe('ToastMessages', () => { ...@@ -41,14 +39,14 @@ describe('ToastMessages', () => {
const wrapper = createComponent(); const wrapper = createComponent();
// We push some messages first // We push some messages first
emitter.emit('toastMessageAdded', fakeMessage('someId1')); emitter.publish('toastMessageAdded', fakeMessage('someId1'));
emitter.emit('toastMessageAdded', fakeMessage('someId2')); emitter.publish('toastMessageAdded', fakeMessage('someId2'));
emitter.emit('toastMessageAdded', fakeMessage('someId3')); emitter.publish('toastMessageAdded', fakeMessage('someId3'));
wrapper.update(); wrapper.update();
emitter.emit('toastMessageDismissed', 'someId1'); emitter.publish('toastMessageDismissed', 'someId1');
// We can also "dismiss" unknown messages. Those will be ignored // We can also "dismiss" unknown messages. Those will be ignored
emitter.emit('toastMessageDismissed', 'someId4'); emitter.publish('toastMessageDismissed', 'someId4');
wrapper.update(); wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 2); assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 2);
......
import * as Hammer from 'hammerjs'; import * as Hammer from 'hammerjs';
import { render } from 'preact'; import { render } from 'preact';
import type { ToastMessage } from '../shared/components/BaseToastMessages';
import { addConfigFragment } from '../shared/config-fragment'; import { addConfigFragment } from '../shared/config-fragment';
import { sendErrorsTo } from '../shared/frame-error-capture'; import { sendErrorsTo } from '../shared/frame-error-capture';
import { ListenerCollection } from '../shared/listener-collection'; import { ListenerCollection } from '../shared/listener-collection';
...@@ -195,10 +196,7 @@ export class Sidebar implements Destroyable { ...@@ -195,10 +196,7 @@ export class Sidebar implements Destroyable {
// will forward messages to render here while it is collapsed. // will forward messages to render here while it is collapsed.
this._messagesElement = document.createElement('div'); this._messagesElement = document.createElement('div');
shadowRoot.appendChild(this._messagesElement); shadowRoot.appendChild(this._messagesElement);
render( render(<ToastMessages emitter={this._emitter} />, this._messagesElement);
<ToastMessages sidebarRPC={this._sidebarRPC} />,
this._messagesElement
);
} }
// Register the sidebar as a handler for Hypothesis errors in this frame. // Register the sidebar as a handler for Hypothesis errors in this frame.
...@@ -391,7 +389,7 @@ export class Sidebar implements Destroyable { ...@@ -391,7 +389,7 @@ export class Sidebar implements Destroyable {
this._sidebarRPC.on('closeSidebar', () => this.close()); this._sidebarRPC.on('closeSidebar', () => this.close());
// Sidebar listens to the `openNotebook` and `openProfile` events coming // Sidebar listens to the `openNotebook` and `openProfile` events coming
// from the sidebar's iframe and re-publishes it via the emitter to the // from the sidebar's iframe and re-publishes them via the emitter to the
// Notebook/Profile // Notebook/Profile
this._sidebarRPC.on('openNotebook', (groupId: string) => { this._sidebarRPC.on('openNotebook', (groupId: string) => {
this.hide(); this.hide();
...@@ -409,6 +407,16 @@ export class Sidebar implements Destroyable { ...@@ -409,6 +407,16 @@ export class Sidebar implements Destroyable {
this.show(); this.show();
}); });
// Sidebar listens to the `toastMessageAdded` and `toastMessageDismissed`
// events coming from the sidebar's iframe and re-publishes them via the
// emitter
this._sidebarRPC.on('toastMessageAdded', (newMessage: ToastMessage) => {
this._emitter.publish('toastMessageAdded', newMessage);
});
this._sidebarRPC.on('toastMessageDismissed', (messageId: string) => {
this._emitter.publish('toastMessageDismissed', messageId);
});
// Suppressing ban-types here because the functions are originally defined // Suppressing ban-types here because the functions are originally defined
// as `Function` somewhere else. To be fixed when that is migrated to TS // as `Function` somewhere else. To be fixed when that is migrated to TS
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
......
...@@ -402,6 +402,28 @@ describe('Sidebar', () => { ...@@ -402,6 +402,28 @@ describe('Sidebar', () => {
}); });
}); });
describe('on "toastMessageAdded" event', () => {
it('re-publishes event via emitter', () => {
const sidebar = createSidebar();
sinon.stub(sidebar._emitter, 'publish');
emitSidebarEvent('toastMessageAdded', {});
assert.calledWith(sidebar._emitter.publish, 'toastMessageAdded', {});
});
});
describe('on "toastMessageDismissed" event', () => {
it('re-publishes event via emitter', () => {
const sidebar = createSidebar();
sinon.stub(sidebar._emitter, 'publish');
emitSidebarEvent('toastMessageDismissed', 'someId');
assert.calledWith(
sidebar._emitter.publish,
'toastMessageDismissed',
'someId'
);
});
});
describe('on "loginRequested" event', () => { describe('on "loginRequested" event', () => {
it('calls the onLoginRequest callback function if one was provided', () => { it('calls the onLoginRequest callback function if one was provided', () => {
const onLoginRequest = sandbox.stub(); const onLoginRequest = sandbox.stub();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment