From 9906ff6f29035da9aeab1b7555a60390e73a6ade Mon Sep 17 00:00:00 2001 From: Lyza Danger Gardner Date: Thu, 29 Oct 2020 15:35:08 -0400 Subject: [PATCH] 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. --- src/annotator/plugin/bucket-bar.js | 144 +--------- src/annotator/plugin/test/bucket-bar-test.js | 274 ++----------------- src/annotator/util/buckets.js | 26 +- src/annotator/util/test/buckets-test.js | 79 ++++-- src/styles/annotator/bucket-bar.scss | 138 ++-------- 5 files changed, 122 insertions(+), 539 deletions(-) diff --git a/src/annotator/plugin/bucket-bar.js b/src/annotator/plugin/bucket-bar.js index 2945fbe8ae6..eda3c942ad5 100644 --- a/src/annotator/plugin/bucket-bar.js +++ b/src/annotator/plugin/bucket-bar.js @@ -1,20 +1,9 @@ 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( + + 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 = `
${this.buckets[index].anchors.length}
`; - 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 + ); } } diff --git a/src/annotator/plugin/test/bucket-bar-test.js b/src/annotator/plugin/test/bucket-bar-test.js index 88528dafc12..19077ec6f7b 100644 --- a/src/annotator/plugin/test/bucket-bar-test.js +++ b/src/annotator/plugin/test/bucket-bar-test.js @@ -1,38 +1,12 @@ 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'); - }); - }); - }); }); diff --git a/src/annotator/util/buckets.js b/src/annotator/util/buckets.js index 06882e0cae6..0d2c5acd35a 100644 --- a/src/annotator/util/buckets.js +++ b/src/annotator/util/buckets.js @@ -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, + }; } diff --git a/src/annotator/util/test/buckets-test.js b/src/annotator/util/test/buckets-test.js index 5c08b9aba1b..164a57d5ee6 100644 --- a/src/annotator/util/test/buckets-test.js +++ b/src/annotator/util/test/buckets-test.js @@ -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], diff --git a/src/styles/annotator/bucket-bar.scss b/src/styles/annotator/bucket-bar.scss index 76d1c4224d4..a9ae06a6445 100644 --- a/src/styles/annotator/bucket-bar.scss +++ b/src/styles/annotator/bucket-bar.scss @@ -1,3 +1,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; } }