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';
import BaseToastMessages from '../../shared/components/BaseToastMessages';
import type { ToastMessage } from '../../shared/components/BaseToastMessages';
import type { PortRPC } from '../../shared/messaging';
import type {
HostToSidebarEvent,
SidebarToHostEvent,
} from '../../types/port-rpc-events';
import type { Emitter } from '../util/emitter';
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.
* 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 addMessage = useCallback(
(newMessage: ToastMessage) => setMessages(prev => [...prev, newMessage]),
......@@ -30,9 +26,14 @@ export default function ToastMessages({ sidebarRPC }: ToastMessagesProps) {
);
useEffect(() => {
sidebarRPC.on('toastMessageAdded', addMessage);
sidebarRPC.on('toastMessageDismissed', dismissMessage);
}, [sidebarRPC, dismissMessage, addMessage]);
emitter.subscribe('toastMessageAdded', addMessage);
emitter.subscribe('toastMessageDismissed', dismissMessage);
return () => {
emitter.unsubscribe('toastMessageAdded', addMessage);
emitter.unsubscribe('toastMessageDismissed', dismissMessage);
};
}, [emitter, dismissMessage, addMessage]);
return (
<BaseToastMessages messages={messages} onMessageDismiss={dismissMessage} />
......
import { mount } from 'enzyme';
import EventEmitter from 'tiny-emitter';
import { Emitter } from '../../util/emitter';
import ToastMessages from '../ToastMessages';
describe('ToastMessages', () => {
let emitter;
let fakeSidebarRPC;
const fakeMessage = (id = 'someId') => ({
id,
......@@ -15,23 +15,21 @@ describe('ToastMessages', () => {
moreInfoURL: 'http://www.example.com',
});
const createComponent = () =>
mount(<ToastMessages sidebarRPC={fakeSidebarRPC} />);
const createComponent = () => mount(<ToastMessages emitter={emitter} />);
beforeEach(() => {
emitter = new EventEmitter();
fakeSidebarRPC = { on: (...args) => emitter.on(...args) };
emitter = new Emitter(new EventEmitter());
});
it('pushes new toast messages on toastMessageAdded', () => {
it('adds new toast messages on toastMessageAdded', () => {
const wrapper = createComponent();
// Initially messages is empty
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 0);
emitter.emit('toastMessageAdded', fakeMessage('someId1'));
emitter.emit('toastMessageAdded', fakeMessage('someId2'));
emitter.emit('toastMessageAdded', fakeMessage('someId3'));
emitter.publish('toastMessageAdded', fakeMessage('someId1'));
emitter.publish('toastMessageAdded', fakeMessage('someId2'));
emitter.publish('toastMessageAdded', fakeMessage('someId3'));
wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 3);
......@@ -41,14 +39,14 @@ describe('ToastMessages', () => {
const wrapper = createComponent();
// We push some messages first
emitter.emit('toastMessageAdded', fakeMessage('someId1'));
emitter.emit('toastMessageAdded', fakeMessage('someId2'));
emitter.emit('toastMessageAdded', fakeMessage('someId3'));
emitter.publish('toastMessageAdded', fakeMessage('someId1'));
emitter.publish('toastMessageAdded', fakeMessage('someId2'));
emitter.publish('toastMessageAdded', fakeMessage('someId3'));
wrapper.update();
emitter.emit('toastMessageDismissed', 'someId1');
emitter.publish('toastMessageDismissed', 'someId1');
// We can also "dismiss" unknown messages. Those will be ignored
emitter.emit('toastMessageDismissed', 'someId4');
emitter.publish('toastMessageDismissed', 'someId4');
wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 2);
......
import * as Hammer from 'hammerjs';
import { render } from 'preact';
import type { ToastMessage } from '../shared/components/BaseToastMessages';
import { addConfigFragment } from '../shared/config-fragment';
import { sendErrorsTo } from '../shared/frame-error-capture';
import { ListenerCollection } from '../shared/listener-collection';
......@@ -195,10 +196,7 @@ export class Sidebar implements Destroyable {
// will forward messages to render here while it is collapsed.
this._messagesElement = document.createElement('div');
shadowRoot.appendChild(this._messagesElement);
render(
<ToastMessages sidebarRPC={this._sidebarRPC} />,
this._messagesElement
);
render(<ToastMessages emitter={this._emitter} />, this._messagesElement);
}
// Register the sidebar as a handler for Hypothesis errors in this frame.
......@@ -391,7 +389,7 @@ export class Sidebar implements Destroyable {
this._sidebarRPC.on('closeSidebar', () => this.close());
// 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
this._sidebarRPC.on('openNotebook', (groupId: string) => {
this.hide();
......@@ -409,6 +407,16 @@ export class Sidebar implements Destroyable {
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
// as `Function` somewhere else. To be fixed when that is migrated to TS
// eslint-disable-next-line @typescript-eslint/ban-types
......
......@@ -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', () => {
it('calls the onLoginRequest callback function if one was provided', () => {
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