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';
import propTypes from 'prop-types';
import bridgeEvents from '../../shared/bridge-events';
import { isIE11 } from '../../shared/user-agent';
import serviceConfig from '../service-config';
import useStore from '../store/use-store';
import uiConstants from '../ui-constants';
......@@ -88,6 +89,20 @@ function HypothesisApp({
}
}, [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 () => {
if (serviceConfig(settings)) {
// Let the host page handle the login request
......
......@@ -7,6 +7,7 @@ import mockImportedComponents from '../../../test-util/mock-imported-components'
import HypothesisApp, { $imports } from '../hypothesis-app';
describe('HypothesisApp', () => {
let fakeUserAgent = null;
let fakeStore = null;
let fakeAuth = null;
let fakeBridge = null;
......@@ -72,12 +73,18 @@ describe('HypothesisApp', () => {
call: sinon.stub(),
};
fakeUserAgent = {
isIE11: sinon.stub().returns(false),
};
fakeToastMessenger = {
error: sinon.stub(),
notice: sinon.stub(),
};
$imports.$mock(mockImportedComponents());
$imports.$mock({
'../../shared/user-agent': fakeUserAgent,
'../service-config': fakeServiceConfig,
'../store/use-store': callback => callback(fakeStore),
'../util/session': {
......@@ -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', () => {
const getStatus = wrapper => wrapper.find('TopBar').prop('auth').status;
......
......@@ -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) {
return mount(
<ToastMessages toastMessenger={fakeToastMessenger} {...props} />
......@@ -58,11 +68,12 @@ describe('ToastMessages', () => {
fakeStore.getToastMessages.returns([
fakeSuccessMessage(),
fakeErrorMessage(),
fakeNoticeMessage(),
]);
const wrapper = createComponent();
assert.lengthOf(wrapper.find('ToastMessage'), 2);
assert.lengthOf(wrapper.find('ToastMessage'), 3);
});
describe('`ToastMessage` sub-component', () => {
......@@ -91,9 +102,24 @@ describe('ToastMessages', () => {
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: fakeErrorMessage(), className: 'toast-message--error' },
{ message: fakeNoticeMessage(), className: 'toast-message--notice' },
].forEach(testCase => {
it('should assign a CSS class based on message type', () => {
fakeStore.getToastMessages.returns([testCase.message]);
......@@ -129,6 +155,7 @@ describe('ToastMessages', () => {
[
{ messages: [fakeSuccessMessage()], icons: ['success'] },
{ messages: [fakeErrorMessage()], icons: ['error'] },
{ messages: [fakeNoticeMessage()], icons: ['cancel'] },
{
messages: [fakeSuccessMessage(), fakeErrorMessage()],
icons: ['success', 'error'],
......@@ -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', () => {
beforeEach(() => {
fakeStore.getToastMessages.returns([
fakeSuccessMessage(),
fakeErrorMessage(),
fakeNoticeMessage(),
]);
});
......
......@@ -16,7 +16,7 @@ import SvgIcon from '../../shared/components/svg-icon';
function ToastMessage({ message, onDismiss }) {
// Capitalize the message type for prepending
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
* non-interactive element. This allows sighted users to get the toast message
......@@ -40,11 +40,25 @@ function ToastMessage({ message, onDismiss }) {
)}
>
<div className="toast-message__type">
<SvgIcon name={message.type} className="toast-message__icon" />
<SvgIcon name={iconName} className="toast-message__icon" />
</div>
<div className="toast-message__message">
<strong>{prefix}: </strong>
{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>
</li>
......
......@@ -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', () => {
fakeStore.hasToastMessage.returns(false);
fakeStore.getToastMessage.returns(undefined);
......@@ -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', () => {
it('does not add a new error message if one with the same message text already exists', () => {
fakeStore.hasToastMessage.returns(true);
......
......@@ -15,6 +15,7 @@ const MESSAGE_DISMISS_DELAY = 500;
*
* @typedef {Object} MessageOptions
* @prop {boolean} [autoDismiss=true] - Whether the toast message automatically disappears.
* @prop {string} [moreInfoURL=''] - Optional URL for users to visit for "more info"
*/
// @ngInject
......@@ -47,15 +48,23 @@ export default function toastMessenger(store) {
* @param {string} message - The message to be rendered
* @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)
if (store.hasToastMessage(type, message)) {
if (store.hasToastMessage(type, messageText)) {
return;
}
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) {
// Attempt to dismiss message after a set time period. NB: The message may
......@@ -87,9 +96,20 @@ export default function toastMessenger(store) {
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 {
dismiss,
error,
success,
notice,
};
}
@use "../../mixins/panel";
@use "../mixins/responsive";
@use "../../variables" as var;
.toast-messages {
position: absolute;
z-index: 1;
width: 100%;
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 {
......@@ -40,7 +51,13 @@
border: 1px solid var.$color-error;
}
&--notice {
border: 1px solid var.$color-notice;
}
&__type {
display: flex;
align-items: center;
padding: 1em;
color: white;
}
......@@ -53,6 +70,10 @@
background-color: var.$color-error;
}
&--notice &__type {
background-color: var.$color-notice;
}
&__icon {
// Specific adjustments for success and error icons
margin-top: 2px;
......@@ -60,6 +81,12 @@
&__message {
padding: 1em;
width: 100%;
}
&__link {
text-align: right;
text-decoration: underline;
}
}
......
......@@ -35,19 +35,16 @@ $grey-6: #3f3f3f;
$grey-7: #202020;
// Colors
$color-success: #00a36d;
$color-warning: #fbc168;
$color-notice: #fbc168;
$color-error: #d93c3f;
$brand: #bd1c2b;
$highlight: #58cef4;
$color-focus-outline: #51a7e8;
$color-focus-shadow: color.scale($color-focus-outline, $alpha: -50%);
$error-color: #f0480c !default;
$success-color: #1cbd41 !default;
// Tint and shade functions from
// https://css-tricks.com/snippets/sass/tint-shade-functions
@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