Unverified Commit 612a583e authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #2208 from hypothesis/ie11-deprecation-toast

Add deprecation notice for IE11 browsers
parents 81e6c501 b3ddff67
...@@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'preact/hooks'; ...@@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'preact/hooks';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import bridgeEvents from '../../shared/bridge-events'; import bridgeEvents from '../../shared/bridge-events';
import { isIE11 } from '../../shared/user-agent';
import serviceConfig from '../service-config'; import serviceConfig from '../service-config';
import useStore from '../store/use-store'; import useStore from '../store/use-store';
import uiConstants from '../ui-constants'; import uiConstants from '../ui-constants';
...@@ -88,6 +89,20 @@ function HypothesisApp({ ...@@ -88,6 +89,20 @@ function HypothesisApp({
} }
}, [isSidebar, profile, openSidebarPanel, settings]); }, [isSidebar, profile, openSidebarPanel, settings]);
// Show a deprecation warning if current browser is IE11
useEffect(() => {
if (isIE11()) {
toastMessenger.notice(
'Hypothesis is ending support for this browser (Internet Explorer 11) on July 1, 2020.',
{
autoDismiss: false,
moreInfoURL:
'https://web.hypothes.is/help/which-browsers-are-supported-by-hypothesis/',
}
);
}
}, [toastMessenger]);
const login = async () => { const login = async () => {
if (serviceConfig(settings)) { if (serviceConfig(settings)) {
// Let the host page handle the login request // Let the host page handle the login request
......
...@@ -7,6 +7,7 @@ import mockImportedComponents from '../../../test-util/mock-imported-components' ...@@ -7,6 +7,7 @@ import mockImportedComponents from '../../../test-util/mock-imported-components'
import HypothesisApp, { $imports } from '../hypothesis-app'; import HypothesisApp, { $imports } from '../hypothesis-app';
describe('HypothesisApp', () => { describe('HypothesisApp', () => {
let fakeUserAgent = null;
let fakeStore = null; let fakeStore = null;
let fakeAuth = null; let fakeAuth = null;
let fakeBridge = null; let fakeBridge = null;
...@@ -72,12 +73,18 @@ describe('HypothesisApp', () => { ...@@ -72,12 +73,18 @@ describe('HypothesisApp', () => {
call: sinon.stub(), call: sinon.stub(),
}; };
fakeUserAgent = {
isIE11: sinon.stub().returns(false),
};
fakeToastMessenger = { fakeToastMessenger = {
error: sinon.stub(), error: sinon.stub(),
notice: sinon.stub(),
}; };
$imports.$mock(mockImportedComponents()); $imports.$mock(mockImportedComponents());
$imports.$mock({ $imports.$mock({
'../../shared/user-agent': fakeUserAgent,
'../service-config': fakeServiceConfig, '../service-config': fakeServiceConfig,
'../store/use-store': callback => callback(fakeStore), '../store/use-store': callback => callback(fakeStore),
'../util/session': { '../util/session': {
...@@ -138,6 +145,24 @@ describe('HypothesisApp', () => { ...@@ -138,6 +145,24 @@ describe('HypothesisApp', () => {
}); });
}); });
describe('toast message warning of IE11 deprecation', () => {
it('shows notice if user agent is IE11', () => {
fakeUserAgent.isIE11.returns(true);
createComponent();
assert.called(fakeToastMessenger.notice);
});
it('does not show notice if another user agent', () => {
fakeUserAgent.isIE11.returns(false);
createComponent();
assert.notCalled(fakeToastMessenger.notice);
});
});
describe('"status" field of "auth" prop passed to children', () => { describe('"status" field of "auth" prop passed to children', () => {
const getStatus = wrapper => wrapper.find('TopBar').prop('auth').status; const getStatus = wrapper => wrapper.find('TopBar').prop('auth').status;
......
...@@ -29,6 +29,16 @@ describe('ToastMessages', () => { ...@@ -29,6 +29,16 @@ describe('ToastMessages', () => {
}; };
}; };
let fakeNoticeMessage = () => {
return {
type: 'notice',
message: 'you should know...',
id: 'someid3',
isDismissed: false,
moreInfoURL: 'http://www.example.com',
};
};
function createComponent(props) { function createComponent(props) {
return mount( return mount(
<ToastMessages toastMessenger={fakeToastMessenger} {...props} /> <ToastMessages toastMessenger={fakeToastMessenger} {...props} />
...@@ -58,11 +68,12 @@ describe('ToastMessages', () => { ...@@ -58,11 +68,12 @@ describe('ToastMessages', () => {
fakeStore.getToastMessages.returns([ fakeStore.getToastMessages.returns([
fakeSuccessMessage(), fakeSuccessMessage(),
fakeErrorMessage(), fakeErrorMessage(),
fakeNoticeMessage(),
]); ]);
const wrapper = createComponent(); const wrapper = createComponent();
assert.lengthOf(wrapper.find('ToastMessage'), 2); assert.lengthOf(wrapper.find('ToastMessage'), 3);
}); });
describe('`ToastMessage` sub-component', () => { describe('`ToastMessage` sub-component', () => {
...@@ -91,9 +102,24 @@ describe('ToastMessages', () => { ...@@ -91,9 +102,24 @@ describe('ToastMessages', () => {
assert.calledOnce(fakeToastMessenger.dismiss); assert.calledOnce(fakeToastMessenger.dismiss);
}); });
it('should not dismiss the message if a "More info" link is clicked', () => {
fakeStore.getToastMessages.returns([fakeNoticeMessage()]);
const wrapper = createComponent();
const link = wrapper.find('.toast-message__link a');
act(() => {
link.getDOMNode().dispatchEvent(new Event('click', { bubbles: true }));
});
assert.notCalled(fakeToastMessenger.dismiss);
});
[ [
{ message: fakeSuccessMessage(), className: 'toast-message--success' }, { message: fakeSuccessMessage(), className: 'toast-message--success' },
{ message: fakeErrorMessage(), className: 'toast-message--error' }, { message: fakeErrorMessage(), className: 'toast-message--error' },
{ message: fakeNoticeMessage(), className: 'toast-message--notice' },
].forEach(testCase => { ].forEach(testCase => {
it('should assign a CSS class based on message type', () => { it('should assign a CSS class based on message type', () => {
fakeStore.getToastMessages.returns([testCase.message]); fakeStore.getToastMessages.returns([testCase.message]);
...@@ -129,6 +155,7 @@ describe('ToastMessages', () => { ...@@ -129,6 +155,7 @@ describe('ToastMessages', () => {
[ [
{ messages: [fakeSuccessMessage()], icons: ['success'] }, { messages: [fakeSuccessMessage()], icons: ['success'] },
{ messages: [fakeErrorMessage()], icons: ['error'] }, { messages: [fakeErrorMessage()], icons: ['error'] },
{ messages: [fakeNoticeMessage()], icons: ['cancel'] },
{ {
messages: [fakeSuccessMessage(), fakeErrorMessage()], messages: [fakeSuccessMessage(), fakeErrorMessage()],
icons: ['success', 'error'], icons: ['success', 'error'],
...@@ -148,11 +175,22 @@ describe('ToastMessages', () => { ...@@ -148,11 +175,22 @@ describe('ToastMessages', () => {
}); });
}); });
it('should render a "more info" link if URL is present in message object', () => {
fakeStore.getToastMessages.returns([fakeNoticeMessage()]);
const wrapper = createComponent();
const link = wrapper.find('.toast-message__link a');
assert.equal(link.props().href, 'http://www.example.com');
assert.equal(link.text(), 'More info');
});
describe('a11y', () => { describe('a11y', () => {
beforeEach(() => { beforeEach(() => {
fakeStore.getToastMessages.returns([ fakeStore.getToastMessages.returns([
fakeSuccessMessage(), fakeSuccessMessage(),
fakeErrorMessage(), fakeErrorMessage(),
fakeNoticeMessage(),
]); ]);
}); });
......
...@@ -16,7 +16,7 @@ import SvgIcon from '../../shared/components/svg-icon'; ...@@ -16,7 +16,7 @@ import SvgIcon from '../../shared/components/svg-icon';
function ToastMessage({ message, onDismiss }) { function ToastMessage({ message, onDismiss }) {
// Capitalize the message type for prepending // Capitalize the message type for prepending
const prefix = message.type.charAt(0).toUpperCase() + message.type.slice(1); const prefix = message.type.charAt(0).toUpperCase() + message.type.slice(1);
const iconName = message.type === 'notice' ? 'cancel' : message.type;
/** /**
* a11y linting is disabled here: There is a click-to-remove handler on a * a11y linting is disabled here: There is a click-to-remove handler on a
* non-interactive element. This allows sighted users to get the toast message * non-interactive element. This allows sighted users to get the toast message
...@@ -40,11 +40,25 @@ function ToastMessage({ message, onDismiss }) { ...@@ -40,11 +40,25 @@ function ToastMessage({ message, onDismiss }) {
)} )}
> >
<div className="toast-message__type"> <div className="toast-message__type">
<SvgIcon name={message.type} className="toast-message__icon" /> <SvgIcon name={iconName} className="toast-message__icon" />
</div> </div>
<div className="toast-message__message"> <div className="toast-message__message">
<strong>{prefix}: </strong> <strong>{prefix}: </strong>
{message.message} {message.message}
{message.moreInfoURL && (
<div className="toast-message__link">
<a
href={message.moreInfoURL}
onClick={
event =>
event.stopPropagation() /* consume the event so that it does not dismiss the message */
}
target="_new"
>
More info
</a>
</div>
)}
</div> </div>
</div> </div>
</li> </li>
......
...@@ -47,6 +47,21 @@ describe('toastMessenger', function () { ...@@ -47,6 +47,21 @@ describe('toastMessenger', function () {
); );
}); });
it('passes along `moreInfoURL` when present', () => {
fakeStore.hasToastMessage.returns(false);
service.success('hooray', { moreInfoURL: 'http://www.example.com' });
assert.calledWith(
fakeStore.addToastMessage,
sinon.match({
type: 'success',
message: 'hooray',
moreInfoURL: 'http://www.example.com',
})
);
});
it('dismisses the message after timeout fires', () => { it('dismisses the message after timeout fires', () => {
fakeStore.hasToastMessage.returns(false); fakeStore.hasToastMessage.returns(false);
fakeStore.getToastMessage.returns(undefined); fakeStore.getToastMessage.returns(undefined);
...@@ -62,6 +77,19 @@ describe('toastMessenger', function () { ...@@ -62,6 +77,19 @@ describe('toastMessenger', function () {
}); });
}); });
describe('#notice', () => {
it('adds a new notice toast message to the store', () => {
fakeStore.hasToastMessage.returns(false);
service.notice('boo');
assert.calledWith(
fakeStore.addToastMessage,
sinon.match({ type: 'notice', message: 'boo' })
);
});
});
describe('#error', () => { describe('#error', () => {
it('does not add a new error message if one with the same message text already exists', () => { it('does not add a new error message if one with the same message text already exists', () => {
fakeStore.hasToastMessage.returns(true); fakeStore.hasToastMessage.returns(true);
......
...@@ -15,6 +15,7 @@ const MESSAGE_DISMISS_DELAY = 500; ...@@ -15,6 +15,7 @@ const MESSAGE_DISMISS_DELAY = 500;
* *
* @typedef {Object} MessageOptions * @typedef {Object} MessageOptions
* @prop {boolean} [autoDismiss=true] - Whether the toast message automatically disappears. * @prop {boolean} [autoDismiss=true] - Whether the toast message automatically disappears.
* @prop {string} [moreInfoURL=''] - Optional URL for users to visit for "more info"
*/ */
// @ngInject // @ngInject
...@@ -47,15 +48,23 @@ export default function toastMessenger(store) { ...@@ -47,15 +48,23 @@ export default function toastMessenger(store) {
* @param {string} message - The message to be rendered * @param {string} message - The message to be rendered
* @param {MessageOptions} [options] * @param {MessageOptions} [options]
*/ */
const addMessage = (type, message, { autoDismiss = true } = {}) => { const addMessage = (
type,
messageText,
{ autoDismiss = true, moreInfoURL = '' } = {}
) => {
// Do not add duplicate messages (messages with the same type and text) // Do not add duplicate messages (messages with the same type and text)
if (store.hasToastMessage(type, message)) { if (store.hasToastMessage(type, messageText)) {
return; return;
} }
const id = generateHexString(10); const id = generateHexString(10);
const message = { type, id, message: messageText, moreInfoURL };
store.addToastMessage({ type, message, id, isDismissed: false }); store.addToastMessage({
isDismissed: false,
...message,
});
if (autoDismiss) { if (autoDismiss) {
// Attempt to dismiss message after a set time period. NB: The message may // Attempt to dismiss message after a set time period. NB: The message may
...@@ -87,9 +96,20 @@ export default function toastMessenger(store) { ...@@ -87,9 +96,20 @@ export default function toastMessenger(store) {
addMessage('success', messageText, options); addMessage('success', messageText, options);
}; };
/**
* Add a warn/notice toast message with `messageText`
*
* @param {string} messageText
* @param {MessageOptions} options
*/
const notice = (messageText, options) => {
addMessage('notice', messageText, options);
};
return { return {
dismiss, dismiss,
error, error,
success, success,
notice,
}; };
} }
@use "../../mixins/panel"; @use "../../mixins/panel";
@use "../mixins/responsive";
@use "../../variables" as var; @use "../../variables" as var;
.toast-messages { .toast-messages {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
width: 100%;
left: 0; left: 0;
padding: 0 8px; width: 100%;
padding: 0 0.5em;
@include responsive.tablet-and-up {
// When displaying in a wider viewport (desktop/tablet outside of sidebar)
max-width: responsive.$break-tablet;
width: responsive.$break-tablet;
padding-left: 2rem;
padding-right: 2rem;
left: 50%;
margin-left: calc(-0.5 * #{responsive.$break-tablet});
}
} }
.toast-message-container { .toast-message-container {
...@@ -40,7 +51,13 @@ ...@@ -40,7 +51,13 @@
border: 1px solid var.$color-error; border: 1px solid var.$color-error;
} }
&--notice {
border: 1px solid var.$color-notice;
}
&__type { &__type {
display: flex;
align-items: center;
padding: 1em; padding: 1em;
color: white; color: white;
} }
...@@ -53,6 +70,10 @@ ...@@ -53,6 +70,10 @@
background-color: var.$color-error; background-color: var.$color-error;
} }
&--notice &__type {
background-color: var.$color-notice;
}
&__icon { &__icon {
// Specific adjustments for success and error icons // Specific adjustments for success and error icons
margin-top: 2px; margin-top: 2px;
...@@ -60,6 +81,12 @@ ...@@ -60,6 +81,12 @@
&__message { &__message {
padding: 1em; padding: 1em;
width: 100%;
}
&__link {
text-align: right;
text-decoration: underline;
} }
} }
......
...@@ -35,19 +35,16 @@ $grey-6: #3f3f3f; ...@@ -35,19 +35,16 @@ $grey-6: #3f3f3f;
$grey-7: #202020; $grey-7: #202020;
// Colors // Colors
$color-success: #00a36d; $color-success: #00a36d;
$color-warning: #fbc168; $color-notice: #fbc168;
$color-error: #d93c3f; $color-error: #d93c3f;
$brand: #bd1c2b; $brand: #bd1c2b;
$highlight: #58cef4; $highlight: #58cef4;
$color-focus-outline: #51a7e8; $color-focus-outline: #51a7e8;
$color-focus-shadow: color.scale($color-focus-outline, $alpha: -50%); $color-focus-shadow: color.scale($color-focus-outline, $alpha: -50%);
$error-color: #f0480c !default;
$success-color: #1cbd41 !default;
// Tint and shade functions from // Tint and shade functions from
// https://css-tricks.com/snippets/sass/tint-shade-functions // https://css-tricks.com/snippets/sass/tint-shade-functions
@function tint($color, $percent) { @function tint($color, $percent) {
......
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