Commit 9906ff6f authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Make `BucketBar` use preact `Buckets`

Refactor `BucketBar` to render preact `Buckets` component. Lightly
refactor `anchorBuckets` utility function to return above and below
buckets as separate properties. Use SASS mixin for indicator button
styling.
parent 1bd15661
import Delegator from '../delegator';
import scrollIntoView from 'scroll-into-view';
import { setHighlightsFocused } from '../highlighter';
import { findClosestOffscreenAnchor, anchorBuckets } from '../util/buckets';
import { createElement, render } from 'preact';
import Buckets from '../components/buckets';
/**
* @typedef {import('../util/buckets').Bucket} Bucket
*/
// Scroll to the next closest anchor off screen in the given direction.
function scrollToClosest(anchors, direction) {
const closest = findClosestOffscreenAnchor(anchors, direction);
if (closest && closest.highlights?.length) {
scrollIntoView(closest.highlights[0]);
}
}
import { anchorBuckets } from '../util/buckets';
export default class BucketBar extends Delegator {
constructor(element, options, annotator) {
......@@ -31,11 +20,6 @@ export default class BucketBar extends Delegator {
this.annotator = annotator;
/** @type {Bucket[]} */
this.buckets = [];
/** @type {HTMLElement[]} - Elements created in the bucket bar for each bucket */
this.tabs = [];
// The element to append this plugin's element to; defaults to the provided
// `element` unless a `container` option was provided
let container = /** @type {HTMLElement} */ (element);
......@@ -80,22 +64,6 @@ export default class BucketBar extends Delegator {
});
}
/**
* Focus or unfocus the anchor highlights in the bucket indicated by `index`
*
* @param {number} index - The bucket's index in the `this.buckets` array
* @param {boolean} toggle - Should this set of highlights be focused (or
* un-focused)?
*/
updateHighlightFocus(index, toggle) {
if (index > 0 && this.buckets[index] && !this.isNavigationBucket(index)) {
const bucket = this.buckets[index];
bucket.anchors.forEach(anchor => {
setHighlightsFocused(anchor.highlights || [], toggle);
});
}
}
update() {
if (this._updatePending) {
return;
......@@ -108,101 +76,17 @@ export default class BucketBar extends Delegator {
}
_update() {
this.buckets = anchorBuckets(this.annotator.anchors);
// The following affordances attempt to reuse existing DOM elements
// when reconstructing bucket "tabs" to cut down on the number of elements
// created and added to the DOM
// Only leave as many "tab" elements attached to the DOM as there are
// buckets
this.tabs.slice(this.buckets.length).forEach(tabEl => tabEl.remove());
// And cut the "tabs" collection down to the size of buckets, too
/** @type {HTMLElement[]} */
this.tabs = this.tabs.slice(0, this.buckets.length);
// If the number of "tabs" currently in the DOM is too small (fewer than
// buckets), fill that gap by creating new elements (and adding event
// listeners to them)
this.buckets.slice(this.tabs.length).forEach(() => {
const tabEl = document.createElement('div');
this.tabs.push(tabEl);
// Note that these elements are reused as buckets change, meaning that
// any given tab element will correspond to a different bucket over time.
// However, we know that we have one "tab" per bucket, in order,
// so we can look up the correct bucket for a tab at event time.
// Focus and unfocus highlights on mouse events
tabEl.addEventListener('mousemove', () => {
this.updateHighlightFocus(this.tabs.indexOf(tabEl), true);
});
tabEl.addEventListener('mouseout', () => {
this.updateHighlightFocus(this.tabs.indexOf(tabEl), false);
});
// Select the annotations (in the sidebar)
// that have anchors within the clicked bucket
tabEl.addEventListener('click', event => {
event.stopPropagation();
const index = this.tabs.indexOf(tabEl);
const bucket = this.buckets[index];
if (!bucket) {
return;
const buckets = anchorBuckets(this.annotator.anchors);
render(
<Buckets
above={buckets.above}
below={buckets.below}
buckets={buckets.buckets}
onSelectAnnotations={(annotations, toggle) =>
this.annotator.selectAnnotations(annotations, toggle)
}
if (this.isLower(index)) {
scrollToClosest(bucket.anchors, 'down');
} else if (this.isUpper(index)) {
scrollToClosest(bucket.anchors, 'up');
} else {
const annotations = bucket.anchors.map(anchor => anchor.annotation);
this.annotator.selectAnnotations(
annotations,
event.ctrlKey || event.metaKey
);
}
});
this.element.appendChild(tabEl);
});
this._buildTabs();
}
_buildTabs() {
this.tabs.forEach((tabEl, index) => {
const anchorCount = this.buckets[index].anchors.length;
tabEl.className = 'annotator-bucket-indicator';
tabEl.style.top = `${this.buckets[index].position}px`;
tabEl.style.display = '';
if (anchorCount) {
tabEl.innerHTML = `<div class="label">${this.buckets[index].anchors.length}</div>`;
if (anchorCount === 1) {
tabEl.setAttribute('title', 'Show one annotation');
} else {
tabEl.setAttribute('title', `Show ${anchorCount} annotations`);
}
} else {
tabEl.style.display = 'none';
}
tabEl.classList.toggle('upper', this.isUpper(index));
tabEl.classList.toggle('lower', this.isLower(index));
});
}
isUpper(i) {
return i === 0;
}
isLower(i) {
return i === this.buckets.length - 1;
}
isNavigationBucket(i) {
return this.isUpper(i) || this.isLower(i);
/>,
this.element
);
}
}
import BucketBar from '../bucket-bar';
import { $imports } from '../bucket-bar';
// Return DOM elements for non-empty bucket indicators in a `BucketBar`
// (i.e. bucket tab elements containing 1 or more anchors)
const nonEmptyBuckets = function (bucketBar) {
const buckets = bucketBar.element.querySelectorAll(
'.annotator-bucket-indicator'
);
return Array.from(buckets).filter(bucket => {
const label = bucket.querySelector('.label');
return !!label;
});
};
const createMouseEvent = function (type, { ctrlKey, metaKey } = {}) {
return new MouseEvent(type, { ctrlKey, metaKey });
};
// Create a fake anchor, which is a combination of annotation object and
// associated highlight elements.
const createAnchor = () => {
return {
annotation: { $tag: 'ann1' },
highlights: [document.createElement('span')],
};
};
describe('BucketBar', () => {
const sandbox = sinon.createSandbox();
let fakeAnnotator;
let fakeBucketUtil;
let fakeHighlighter;
let fakeScrollIntoView;
let bucketBar;
let bucketProps;
const createBucketBar = function (options) {
const element = document.createElement('div');
......@@ -40,25 +14,23 @@ describe('BucketBar', () => {
};
beforeEach(() => {
bucketProps = {};
fakeAnnotator = {
anchors: [],
selectAnnotations: sinon.stub(),
};
fakeBucketUtil = {
anchorBuckets: sinon.stub().returns([]),
findClosestOffscreenAnchor: sinon.stub(),
anchorBuckets: sinon.stub().returns({}),
};
fakeHighlighter = {
setHighlightsFocused: sinon.stub(),
const FakeBuckets = props => {
bucketProps = props;
return null;
};
fakeScrollIntoView = sinon.stub();
$imports.$mock({
'scroll-into-view': fakeScrollIntoView,
'../highlighter': fakeHighlighter,
'../components/buckets': FakeBuckets,
'../util/buckets': fakeBucketUtil,
});
......@@ -114,6 +86,15 @@ describe('BucketBar', () => {
assert.calledOnce(fakeBucketUtil.anchorBuckets);
});
it('should select annotations when Buckets component invokes callback', () => {
const fakeAnnotations = ['hi', 'there'];
bucketBar = createBucketBar();
bucketBar._update();
bucketProps.onSelectAnnotations(fakeAnnotations, true);
assert.calledWith(fakeAnnotator.selectAnnotations, fakeAnnotations, true);
});
context('when scrollables provided', () => {
let scrollableEls = [];
......@@ -155,227 +136,4 @@ describe('BucketBar', () => {
assert.notCalled(window.requestAnimationFrame);
});
});
describe('user interactions with buckets', () => {
beforeEach(() => {
bucketBar = createBucketBar();
// Create fake anchors and render buckets.
const anchors = [createAnchor()];
fakeBucketUtil.anchorBuckets.returns([
{ anchors: [], position: 137 }, // Upper navigation
{ anchors: [anchors[0]], position: 250 },
{ anchors: [], position: 400 }, // Lower navigation
]);
bucketBar.annotator.anchors = anchors;
bucketBar.update();
});
it('highlights the bucket anchors when pointer device moved into bucket', () => {
const bucketEls = nonEmptyBuckets(bucketBar);
bucketEls[0].dispatchEvent(createMouseEvent('mousemove'));
assert.calledOnce(fakeHighlighter.setHighlightsFocused);
assert.calledWith(
fakeHighlighter.setHighlightsFocused,
bucketBar.annotator.anchors[0].highlights,
true
);
});
it('un-highlights the bucket anchors when pointer device moved out of bucket', () => {
const bucketEls = nonEmptyBuckets(bucketBar);
bucketEls[0].dispatchEvent(createMouseEvent('mousemove'));
bucketEls[0].dispatchEvent(createMouseEvent('mouseout'));
assert.calledTwice(fakeHighlighter.setHighlightsFocused);
const secondCall = fakeHighlighter.setHighlightsFocused.getCall(1);
assert.equal(
secondCall.args[0],
bucketBar.annotator.anchors[0].highlights
);
assert.equal(secondCall.args[1], false);
});
it('selects the annotations corresponding to the anchors in a bucket when bucket is clicked', () => {
const bucketEls = nonEmptyBuckets(bucketBar);
assert.equal(bucketEls.length, 1);
bucketEls[0].dispatchEvent(createMouseEvent('click'));
const anns = bucketBar.annotator.anchors.map(anchor => anchor.annotation);
assert.calledWith(bucketBar.annotator.selectAnnotations, anns, false);
});
it('handles missing buckets gracefully on click', () => {
// FIXME - refactor and remove necessity for this test
// There is a coupling between `BucketBar.prototype.tabs` and
// `BucketBar.prototype.buckets` — they're "expected" to be the same
// length and correspond to each other. This very much should be the case,
// but, just in case...
const bucketEls = nonEmptyBuckets(bucketBar);
assert.equal(bucketEls.length, 1);
bucketBar.tabs = [];
bucketEls[0].dispatchEvent(createMouseEvent('click'));
assert.notCalled(bucketBar.annotator.selectAnnotations);
});
[
{ ctrlKey: true, metaKey: false },
{ ctrlKey: false, metaKey: true },
].forEach(({ ctrlKey, metaKey }) =>
it('toggles selection of the annotations if Ctrl or Alt is pressed', () => {
const bucketEls = nonEmptyBuckets(bucketBar);
assert.equal(bucketEls.length, 1);
bucketEls[0].dispatchEvent(
createMouseEvent('click', { ctrlKey, metaKey })
);
const anns = bucketBar.annotator.anchors.map(
anchor => anchor.annotation
);
assert.calledWith(bucketBar.annotator.selectAnnotations, anns, true);
})
);
});
describe('rendered bucket "tabs"', () => {
let fakeAnchors;
let fakeAbove;
let fakeBelow;
let fakeBuckets;
beforeEach(() => {
bucketBar = createBucketBar();
fakeAnchors = [
createAnchor(),
createAnchor(),
createAnchor(),
createAnchor(),
createAnchor(),
createAnchor(),
];
// These two anchors are considered to be offscreen upwards
fakeAbove = [fakeAnchors[0], fakeAnchors[1]];
fakeBelow = [fakeAnchors[5]];
// These buckets are on-screen
fakeBuckets = [
{ anchors: fakeAbove, position: 137 },
{ anchors: [fakeAnchors[2], fakeAnchors[3]], position: 350 },
{ anchors: [fakeAnchors[4]], position: 550 },
{ anchors: fakeBelow, position: 600 },
];
// This anchor is offscreen below
fakeBucketUtil.anchorBuckets.returns(fakeBuckets.slice());
});
describe('navigation bucket tabs', () => {
it('adds navigation tabs to scroll up and down to nearest anchors offscreen', () => {
bucketBar.update();
const validBuckets = nonEmptyBuckets(bucketBar);
assert.equal(
validBuckets[0].getAttribute('title'),
'Show 2 annotations'
);
assert.equal(
validBuckets[validBuckets.length - 1].getAttribute('title'),
'Show one annotation'
);
assert.isTrue(validBuckets[0].classList.contains('upper'));
assert.isTrue(
validBuckets[validBuckets.length - 1].classList.contains('lower')
);
});
it('removes unneeded tab elements from the document', () => {
bucketBar.update();
const extraEl = document.createElement('div');
extraEl.className = 'extraTab';
bucketBar.element.append(extraEl);
bucketBar.tabs.push(extraEl);
assert.equal(bucketBar.tabs.length, bucketBar.buckets.length + 1);
// Resetting this return is necessary to return a fresh array reference
// on next update
fakeBucketUtil.anchorBuckets.returns(fakeBuckets.slice());
bucketBar.update();
assert.equal(bucketBar.tabs.length, bucketBar.buckets.length);
assert.notExists(bucketBar.element.querySelector('.extraTab'));
});
it('scrolls up to nearest anchor above when upper navigation tab clicked', () => {
fakeBucketUtil.findClosestOffscreenAnchor.returns(fakeAnchors[1]);
bucketBar.update();
const visibleBuckets = nonEmptyBuckets(bucketBar);
visibleBuckets[0].dispatchEvent(createMouseEvent('click'));
assert.calledOnce(fakeBucketUtil.findClosestOffscreenAnchor);
assert.calledWith(
fakeBucketUtil.findClosestOffscreenAnchor,
sinon.match([fakeAnchors[0], fakeAnchors[1]]),
'up'
);
assert.calledOnce(fakeScrollIntoView);
assert.calledWith(fakeScrollIntoView, fakeAnchors[1].highlights[0]);
});
it('scrolls down to nearest anchor below when lower navigation tab clicked', () => {
fakeBucketUtil.findClosestOffscreenAnchor.returns(fakeAnchors[5]);
bucketBar.update();
const visibleBuckets = nonEmptyBuckets(bucketBar);
visibleBuckets[visibleBuckets.length - 1].dispatchEvent(
createMouseEvent('click')
);
assert.calledOnce(fakeBucketUtil.findClosestOffscreenAnchor);
assert.calledWith(
fakeBucketUtil.findClosestOffscreenAnchor,
sinon.match([fakeAnchors[5]]),
'down'
);
assert.calledOnce(fakeScrollIntoView);
assert.calledWith(fakeScrollIntoView, fakeAnchors[5].highlights[0]);
});
});
it('displays bucket tabs that have at least one anchor', () => {
bucketBar.update();
const visibleBuckets = nonEmptyBuckets(bucketBar);
// Visible buckets include: upper navigation tab, two on-screen buckets,
// lower navigation tab = 4
assert.equal(visibleBuckets.length, 4);
visibleBuckets.forEach(visibleEl => {
assert.equal(visibleEl.style.display, '');
});
});
it('sets bucket-tab label text and title based on number of anchors', () => {
bucketBar.update();
const visibleBuckets = nonEmptyBuckets(bucketBar);
// Upper navigation bucket tab
assert.equal(visibleBuckets[0].title, 'Show 2 annotations');
assert.equal(visibleBuckets[0].querySelector('.label').innerHTML, '2');
// First on-screen visible bucket
assert.equal(visibleBuckets[1].title, 'Show 2 annotations');
assert.equal(visibleBuckets[1].querySelector('.label').innerHTML, '2');
// Second on-screen visible bucket
assert.equal(visibleBuckets[2].title, 'Show one annotation');
assert.equal(visibleBuckets[2].querySelector('.label').innerHTML, '1');
// Lower navigation bucket tab
assert.equal(visibleBuckets[3].title, 'Show one annotation');
assert.equal(visibleBuckets[3].querySelector('.label').innerHTML, '1');
});
it('does not display empty bucket tabs', () => {
fakeBucketUtil.anchorBuckets.returns([]);
bucketBar.update();
const allBuckets = bucketBar.element.querySelectorAll(
'.annotator-bucket-indicator'
);
// All of the buckets are empty...
allBuckets.forEach(bucketEl => {
assert.equal(bucketEl.style.display, 'none');
});
});
});
});
......@@ -11,6 +11,15 @@ import { getBoundingClientRect } from '../highlighter';
* appear in the bucket bar.
*/
/**
* @typedef BucketSet
* @prop {Bucket} above - A single bucket containing all of the anchors that
* are offscreen upwards
* @prop {Bucket} below - A single bucket containing all of the anchors that are
* offscreen downwards
* @prop {Bucket[]} buckets - On-screen buckets
*/
/**
* @typedef WorkingBucket
* @prop {Anchor[]} anchors - The anchors in this bucket
......@@ -131,7 +140,7 @@ function getAnchorPositions(anchors) {
* Compute buckets
*
* @param {Anchor[]} anchors
* @return {Bucket[]}
* @return {BucketSet}
*/
export function anchorBuckets(anchors) {
const anchorPositions = getAnchorPositions(anchors);
......@@ -214,15 +223,20 @@ export function anchorBuckets(anchors) {
}
// Add an upper "navigation" bucket with offscreen-above anchors
buckets.unshift({
const above = {
anchors: Array.from(aboveScreen),
position: BUCKET_TOP_THRESHOLD,
});
};
// Add a lower "navigation" bucket with offscreen-below anchors
buckets.push({
const below = {
anchors: Array.from(belowScreen),
position: window.innerHeight - BUCKET_BOTTOM_THRESHOLD,
});
return buckets;
};
return {
above,
below,
buckets,
};
}
......@@ -114,57 +114,66 @@ describe('annotator/util/buckets', () => {
});
describe('anchorBuckets', () => {
it('puts anchors that are above the screen into the first bucket', () => {
const buckets = anchorBuckets(fakeAnchors);
assert.deepEqual(buckets[0].anchors, [fakeAnchors[0], fakeAnchors[1]]);
it('puts anchors that are above the screen into the `above` bucket', () => {
const bucketSet = anchorBuckets(fakeAnchors);
assert.deepEqual(bucketSet.above.anchors, [
fakeAnchors[0],
fakeAnchors[1],
]);
});
it('puts anchors that are below the screen into the last bucket', () => {
const buckets = anchorBuckets(fakeAnchors);
assert.deepEqual(buckets[buckets.length - 1].anchors, [
it('puts anchors that are below the screen into the `below` bucket', () => {
const bucketSet = anchorBuckets(fakeAnchors);
assert.deepEqual(bucketSet.below.anchors, [
fakeAnchors[4],
fakeAnchors[5],
]);
});
it('puts on-screen anchors into a bucket', () => {
const buckets = anchorBuckets(fakeAnchors);
assert.deepEqual(buckets[1].anchors, [fakeAnchors[2], fakeAnchors[3]]);
it('puts on-screen anchors into a buckets', () => {
const bucketSet = anchorBuckets(fakeAnchors);
assert.deepEqual(bucketSet.buckets[0].anchors, [
fakeAnchors[2],
fakeAnchors[3],
]);
});
it('puts anchors into separate buckets if more than 60px separates their boxes', () => {
fakeAnchors[2].highlights = [201, 15]; // bottom 216
fakeAnchors[3].highlights = [301, 15]; // top 301 - more than 60px from 216
const buckets = anchorBuckets(fakeAnchors);
assert.deepEqual(buckets[1].anchors, [fakeAnchors[2]]);
assert.deepEqual(buckets[2].anchors, [fakeAnchors[3]]);
const bucketSet = anchorBuckets(fakeAnchors);
assert.deepEqual(bucketSet.buckets[0].anchors, [fakeAnchors[2]]);
assert.deepEqual(bucketSet.buckets[1].anchors, [fakeAnchors[3]]);
});
it('puts overlapping anchors into a shared bucket', () => {
fakeAnchors[2].highlights = [201, 200]; // Bottom 401
fakeAnchors[3].highlights = [285, 100]; // Bottom 385
const buckets = anchorBuckets(fakeAnchors);
assert.deepEqual(buckets[1].anchors, [fakeAnchors[2], fakeAnchors[3]]);
const bucketSet = anchorBuckets(fakeAnchors);
assert.deepEqual(bucketSet.buckets[0].anchors, [
fakeAnchors[2],
fakeAnchors[3],
]);
});
it('positions the bucket at v. midpoint of the box containing all bucket anchors', () => {
it('positions the bucket at vertical midpoint of the box containing all bucket anchors', () => {
fakeAnchors[2].highlights = [200, 50]; // Top 200
fakeAnchors[3].highlights = [225, 75]; // Bottom 300
const buckets = anchorBuckets(fakeAnchors);
assert.equal(buckets[1].position, 250);
const bucketSet = anchorBuckets(fakeAnchors);
assert.equal(bucketSet.buckets[0].position, 250);
});
it('only buckets annotations that have highlights', () => {
const badAnchor = { highlights: [] };
fakeAnchors.push(badAnchor);
const buckets = anchorBuckets([badAnchor]);
assert.equal(buckets.length, 2);
assert.isEmpty(buckets[0].anchors); // Holder for above-screen anchors
assert.isEmpty(buckets[1].anchors); // Holder for below-screen anchors
const bucketSet = anchorBuckets([badAnchor]);
assert.equal(bucketSet.buckets.length, 0);
assert.isEmpty(bucketSet.above.anchors); // Holder for above-screen anchors
assert.isEmpty(bucketSet.below.anchors); // Holder for below-screen anchors
});
it('sorts anchors by top position', () => {
const buckets = anchorBuckets([
const bucketSet = anchorBuckets([
fakeAnchors[3],
fakeAnchors[2],
fakeAnchors[5],
......@@ -172,9 +181,18 @@ describe('annotator/util/buckets', () => {
fakeAnchors[0],
fakeAnchors[1],
]);
assert.deepEqual(buckets[0].anchors, [fakeAnchors[0], fakeAnchors[1]]);
assert.deepEqual(buckets[1].anchors, [fakeAnchors[2], fakeAnchors[3]]);
assert.deepEqual(buckets[2].anchors, [fakeAnchors[4], fakeAnchors[5]]);
assert.deepEqual(bucketSet.above.anchors, [
fakeAnchors[0],
fakeAnchors[1],
]);
assert.deepEqual(bucketSet.buckets[0].anchors, [
fakeAnchors[2],
fakeAnchors[3],
]);
assert.deepEqual(bucketSet.below.anchors, [
fakeAnchors[4],
fakeAnchors[5],
]);
});
it('returns only above- and below-screen anchors if none are on-screen', () => {
......@@ -183,12 +201,15 @@ describe('annotator/util/buckets', () => {
fakeAnchors[3].highlights = [1100, 75];
fakeAnchors[4].highlights = [1200, 100];
fakeAnchors[5].highlights = [1300, 75];
const buckets = anchorBuckets(fakeAnchors);
assert.equal(buckets.length, 2);
const bucketSet = anchorBuckets(fakeAnchors);
assert.equal(bucketSet.buckets.length, 0);
// Above-screen
assert.deepEqual(buckets[0].anchors, [fakeAnchors[0], fakeAnchors[1]]);
assert.deepEqual(bucketSet.above.anchors, [
fakeAnchors[0],
fakeAnchors[1],
]);
// Below-screen
assert.deepEqual(buckets[1].anchors, [
assert.deepEqual(bucketSet.below.anchors, [
fakeAnchors[2],
fakeAnchors[3],
fakeAnchors[4],
......
@use "../mixins/buttons";
@use "../mixins/reset";
@use "../mixins/utils";
@use "../variables" as var;
......@@ -20,126 +21,31 @@
background: rgba(0, 0, 0, 0.08);
}
.annotator-bucket-indicator {
box-sizing: border-box;
background: var.$white;
border: solid 1px var.$grey-3;
border-radius: 2px 4px 4px 2px;
right: 0;
pointer-events: all;
.buckets,
.bucket {
position: absolute;
line-height: 1;
height: 16px;
width: 26px;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center;
cursor: pointer;
// Vertically center the element, which is 16px high
margin-top: -8px;
.label {
@include reset.reset-box-model;
@include reset.reset-font;
background: none;
color: var.$color-text--light;
font-weight: bold;
font-family: var.$sans-font-family;
font-size: var.$annotator-bucket-bar-font-size;
line-height: var.$annotator-bucket-bar-line-height;
margin: 0 auto;
}
&:before,
&:after {
content: '';
right: 100%;
top: 50%;
position: absolute;
// NB: use of 'inset' here fixes jagged diagonals in FF
// https://github.com/zurb/foundation/issues/2230
border: inset transparent;
height: 0;
width: 0;
}
&:before {
border-width: 8px;
border-right: 5px solid var.$grey-3;
margin-top: -8px;
}
&:after {
border-width: 7px;
border-right: 4px solid var.$white;
margin-top: -7px;
}
&.lower,
&.upper {
@include utils.shadow;
z-index: 1;
&:before,
&:after {
left: 50%;
bottom: 100%;
right: auto;
border-right: solid transparent;
margin-top: 0;
}
& .label {
// Vertical alignment tweak to better center the label in the indicator
margin-top: -1px;
}
}
&.upper {
border-radius: 2px 2px 4px 4px;
// Vertically center the element (which is 22px high) by adding a negative
// top margin in conjunction with an inline style `top` position (set
// in code)
margin-top: -11px;
&:before,
&:after {
top: auto;
bottom: 100%;
}
&:before {
border-width: 13px;
border-bottom: 6px solid var.$grey-3;
margin-left: -13px;
}
&:after {
border-width: 12px;
border-bottom: 5px solid var.$white;
margin-left: -12px;
}
}
right: 0;
}
&.lower {
margin-top: 0;
border-radius: 4px 4px 2px 2px;
.bucket-button {
// Need pointer events again. Necessary because of `pointer-events` rule
// in `.annotator-bucket-bar`
pointer-events: all;
}
&:before,
&:after {
bottom: auto;
top: 100%;
}
.bucket-button--left {
// Center the indicator vertically (the element is 16px tall)
margin-top: -8px;
@include buttons.indicator--left;
}
&:before {
border-width: 13px;
border-top: 6px solid var.$grey-3;
margin-left: -13px;
}
.bucket-button--up {
@include buttons.indicator--up;
// Vertically center the element (which is 22px high)
margin-top: -11px;
}
&:after {
border-width: 12px;
border-top: 5px solid var.$white;
margin-left: -12px;
}
}
.bucket-button--down {
@include buttons.indicator--down;
}
}
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