Commit 184ff1ab authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Create component to render toast messages in host frame

parent c24514cb
import { useCallback, useEffect, useState } from 'preact/hooks';
import BaseToastMessages from '../../shared/components/BaseToastMessages';
import type { PortRPC } from '../../shared/messaging';
import type { ToastMessage } from '../../sidebar/store/modules/toast-messages';
import type {
HostToSidebarEvent,
SidebarToHostEvent,
} from '../../types/port-rpc-events';
export type HostToastMessagesProps = {
sidebarRPC: PortRPC<SidebarToHostEvent, HostToSidebarEvent>;
};
/**
* A component designed to render toast messages coming 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 }: HostToastMessagesProps) {
const [messages, setMessages] = useState<ToastMessage[]>([]);
const pushMessage = useCallback(
(newMessage: ToastMessage) => setMessages(prev => [...prev, newMessage]),
[]
);
const dismissMessage = useCallback(
(messageId: string) =>
setMessages(prev => prev.filter(message => message.id !== messageId)),
[]
);
useEffect(() => {
sidebarRPC.on('toastMessageAdded', pushMessage);
sidebarRPC.on('toastMessageDismissed', dismissMessage);
}, [sidebarRPC, dismissMessage, pushMessage]);
return (
<BaseToastMessages messages={messages} onMessageDismiss={dismissMessage} />
);
}
import { mount } from 'enzyme';
import EventEmitter from 'tiny-emitter';
import ToastMessages from '../ToastMessages';
describe('ToastMessages', () => {
let emitter;
let fakeSidebarRPC;
const fakeMessage = (id = 'someId') => ({
id,
type: 'notice',
message: 'you should know...',
isDismissed: false,
moreInfoURL: 'http://www.example.com',
});
const createComponent = () =>
mount(<ToastMessages sidebarRPC={fakeSidebarRPC} />);
beforeEach(() => {
emitter = new EventEmitter();
fakeSidebarRPC = { on: (...args) => emitter.on(...args) };
});
it('pushes 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'));
wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 3);
});
it('removes toast existing messages on toastMessageDismissed', () => {
const wrapper = createComponent();
// We push some messages first
emitter.emit('toastMessageAdded', fakeMessage('someId1'));
emitter.emit('toastMessageAdded', fakeMessage('someId2'));
emitter.emit('toastMessageAdded', fakeMessage('someId3'));
wrapper.update();
emitter.emit('toastMessageDismissed', 'someId1');
// We can also "dismiss" unknown messages. Those will be ignored
emitter.emit('toastMessageDismissed', 'someId4');
wrapper.update();
assert.lengthOf(wrapper.find('BaseToastMessages').prop('messages'), 2);
});
});
import * as Hammer from 'hammerjs';
import { render } from 'preact';
import { addConfigFragment } from '../shared/config-fragment';
import { sendErrorsTo } from '../shared/frame-error-capture';
......@@ -18,6 +19,7 @@ import type {
} from '../types/port-rpc-events';
import { annotationCounts } from './annotation-counts';
import { BucketBar } from './bucket-bar';
import ToastMessages from './components/ToastMessages';
import { createAppConfig } from './config/app';
import { FeatureFlags } from './features';
import { sidebarTrigger } from './sidebar-trigger';
......@@ -187,6 +189,12 @@ export class Sidebar implements Destroyable {
shadowRoot.appendChild(this.iframeContainer);
element.appendChild(this._hypothesisSidebar);
// Render a container for toast messages in the host frame. The sidebar
// will forward messages to render here while it is collapsed.
const messagesElement = document.createElement('div');
shadowRoot.appendChild(messagesElement);
render(<ToastMessages sidebarRPC={this._sidebarRPC} />, messagesElement);
}
// Register the sidebar as a handler for Hypothesis errors in this frame.
......
......@@ -8,6 +8,9 @@ export type ToastMessageProps = {
toastMessenger: ToastMessengerService;
};
/**
* A component designed to render toast messages handled by the sidebar store.
*/
function ToastMessages({ toastMessenger }: ToastMessageProps) {
const store = useSidebarStore();
const messages = store.getToastMessages();
......
......@@ -7,15 +7,13 @@ describe('ToastMessages', () => {
let fakeStore;
let fakeToastMessenger;
const fakeMessage = () => {
return {
type: 'notice',
message: 'you should know...',
id: 'someId',
isDismissed: false,
moreInfoURL: 'http://www.example.com',
};
};
const fakeMessage = () => ({
type: 'notice',
message: 'you should know...',
id: 'someId',
isDismissed: false,
moreInfoURL: 'http://www.example.com',
});
function createComponent(props) {
return mount(
......
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