Commit 8587df5c authored by Robert Knight's avatar Robert Knight

Implement a fallback if CSS blending is not supported

Implement a fallback for browsers that don't support the CSS
`mix-blend-mode` property (IE 11, Edge < 79) by merging overlapping
highlights into a single layer with uniform opacity. This prevents
overlapping highlights from affecting readability since highlights are
blended with the content underneath using normal blending in this case.
parent 55e4ad5a
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
function isCSSPropertySupported(property, value) {
if (typeof CSS !== 'function' || typeof CSS.supports !== 'function') {
/* istanbul ignore next */
return false;
}
return CSS.supports(property, value);
}
/** /**
* Polyfill for `element.closest(selector)`, only needed for IE 11. * Polyfill for `element.closest(selector)`, only needed for IE 11.
*/ */
...@@ -73,6 +81,11 @@ function drawHighlightsAbovePdfCanvas(highlightEl) { ...@@ -73,6 +81,11 @@ function drawHighlightsAbovePdfCanvas(highlightEl) {
'.hypothesis-highlight-layer' '.hypothesis-highlight-layer'
); );
const isCssBlendSupported = isCSSPropertySupported(
'mix-blend-mode',
'multiply'
);
if (!svgHighlightLayer) { if (!svgHighlightLayer) {
// Create SVG layer. This must be in the same stacking context as // Create SVG layer. This must be in the same stacking context as
// the canvas so that CSS `mix-blend-mode` can be used to control how SVG // the canvas so that CSS `mix-blend-mode` can be used to control how SVG
...@@ -91,15 +104,20 @@ function drawHighlightsAbovePdfCanvas(highlightEl) { ...@@ -91,15 +104,20 @@ function drawHighlightsAbovePdfCanvas(highlightEl) {
svgStyle.width = '100%'; svgStyle.width = '100%';
svgStyle.height = '100%'; svgStyle.height = '100%';
if (isCssBlendSupported) {
// Use multiply blending so that highlights drawn on top of text darken it // Use multiply blending so that highlights drawn on top of text darken it
// rather than making it lighter. This improves contrast and thus readability // rather than making it lighter. This improves contrast and thus readability
// of highlighted text. This choice optimizes for dark text on a light // of highlighted text, especially for overlapping highlights.
// background, as the most common case.
// //
// Browsers which don't support the `mix-blend-mode` property (IE 11, Edge < 79) // This choice optimizes for the common case of dark text on a light background.
// will use "normal" blending, which is still usable but has reduced contrast,
// especially for overlapping highlights.
svgStyle.mixBlendMode = 'multiply'; svgStyle.mixBlendMode = 'multiply';
} else {
// For older browsers (IE 11, Edge < 79) we draw all the highlights as
// opaque and then make the entire highlight layer transparent. This means
// that there is no visual indication of whether text has one or multiple
// highlights, but it preserves readability.
svgStyle.opacity = 0.3;
}
} }
const canvasRect = canvasEl.getBoundingClientRect(); const canvasRect = canvasEl.getBoundingClientRect();
...@@ -111,7 +129,13 @@ function drawHighlightsAbovePdfCanvas(highlightEl) { ...@@ -111,7 +129,13 @@ function drawHighlightsAbovePdfCanvas(highlightEl) {
rect.setAttribute('y', highlightRect.top - canvasRect.top); rect.setAttribute('y', highlightRect.top - canvasRect.top);
rect.setAttribute('width', highlightRect.width); rect.setAttribute('width', highlightRect.width);
rect.setAttribute('height', highlightRect.height); rect.setAttribute('height', highlightRect.height);
if (isCssBlendSupported) {
rect.setAttribute('class', 'hypothesis-svg-highlight'); rect.setAttribute('class', 'hypothesis-svg-highlight');
} else {
rect.setAttribute('class', 'hypothesis-svg-highlight is-opaque');
}
svgHighlightLayer.appendChild(rect); svgHighlightLayer.appendChild(rect);
return rect; return rect;
......
...@@ -221,6 +221,53 @@ describe('annotator/highlighter', () => { ...@@ -221,6 +221,53 @@ describe('annotator/highlighter', () => {
assert.isNull(container.querySelector('rect')); assert.isNull(container.querySelector('rect'));
assert.notOk(highlight.svgHighlight); assert.notOk(highlight.svgHighlight);
}); });
describe('CSS blend mode support testing', () => {
beforeEach(() => {
sinon.stub(CSS, 'supports');
});
afterEach(() => {
CSS.supports.restore();
});
it('renders highlights when mix-blend-mode is supported', () => {
const container = document.createElement('div');
render(<PdfPage />, container);
CSS.supports.withArgs('mix-blend-mode', 'multiply').returns(true);
highlightPdfRange(container);
// When mix blending is available, the highlight layer has default
// opacity and highlight rects are transparent.
const highlightLayer = container.querySelector(
'.hypothesis-highlight-layer'
);
assert.equal(highlightLayer.style.opacity, '');
const rect = container.querySelector('rect');
assert.equal(rect.getAttribute('class'), 'hypothesis-svg-highlight');
});
it('renders highlights when mix-blend-mode is not supported', () => {
const container = document.createElement('div');
render(<PdfPage />, container);
CSS.supports.withArgs('mix-blend-mode', 'multiply').returns(false);
highlightPdfRange(container);
// When mix blending is not available, highlight rects are opaque and
// the entire highlight layer is transparent.
const highlightLayer = container.querySelector(
'.hypothesis-highlight-layer'
);
assert.equal(highlightLayer.style.opacity, '0.3');
const rect = container.querySelector('rect');
assert.include(
rect.getAttribute('class'),
'hypothesis-svg-highlight is-opaque'
);
});
});
}); });
}); });
......
...@@ -27,6 +27,10 @@ ...@@ -27,6 +27,10 @@
.hypothesis-highlights-always-on { .hypothesis-highlights-always-on {
.hypothesis-svg-highlight { .hypothesis-svg-highlight {
fill: var.$highlight-color; fill: var.$highlight-color;
&.is-opaque {
fill: yellow;
}
} }
.hypothesis-highlight { .hypothesis-highlight {
......
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