From 5616b74a88d9ee143c0e6e224a8d91a4c1fa6868 Mon Sep 17 00:00:00 2001 From: Conrad Chan Date: Tue, 12 Mar 2019 15:31:22 -0700 Subject: [PATCH] Update: Animate Thumbnails Sidebar transition (#948) --- src/index.html | 6 +-- src/lib/ThumbnailsSidebar.js | 25 ++++-------- src/lib/__tests__/ThumbnailsSidebar-test.js | 38 +++++-------------- src/lib/_common.scss | 10 ++++- src/lib/constants.js | 1 + src/lib/viewers/doc/DocBaseViewer.js | 35 +++++++++-------- .../doc/__tests__/DocBaseViewer-test.js | 35 +++++++++++++---- src/lib/viewers/doc/_docBase.scss | 21 +++++++++- .../document/Thumbnails.e2e.test.js | 4 +- 9 files changed, 101 insertions(+), 74 deletions(-) diff --git a/src/index.html b/src/index.html index b5f156732..fef4465aa 100644 --- a/src/index.html +++ b/src/index.html @@ -19,7 +19,7 @@ .setters-container { display: flex; font-size: 75%; - height: 25vh; + height: 10vh; justify-content: space-around; padding: 20px; } @@ -37,8 +37,8 @@ text-align: center; } - #preview-container { - height: 75vh; + .preview-container { + height: 90vh; width: 100vw; } diff --git a/src/lib/ThumbnailsSidebar.js b/src/lib/ThumbnailsSidebar.js index e8af099b9..e13b886a9 100644 --- a/src/lib/ThumbnailsSidebar.js +++ b/src/lib/ThumbnailsSidebar.js @@ -1,6 +1,5 @@ import isFinite from 'lodash/isFinite'; import VirtualScroller from './VirtualScroller'; -import { CLASS_HIDDEN } from './constants'; import BoundedCache from './BoundedCache'; const CLASS_BOX_PREVIEW_THUMBNAIL = 'bp-thumbnail'; @@ -26,6 +25,9 @@ class ThumbnailsSidebar { /** @property {Array} - The list of currently rendered thumbnail elements */ currentThumbnails; + /** @property {Boolean} - Whether the sidebar is open or not */ + isOpen; + /** @property {PDfViewer} - The PDFJS viewer instance */ pdfViewer; @@ -46,6 +48,7 @@ class ThumbnailsSidebar { this.currentThumbnails = []; this.pdfViewer = pdfViewer; this.thumbnailImageCache = new BoundedCache(); + this.isOpen = false; this.createImageEl = this.createImageEl.bind(this); this.createPlaceholderThumbnail = this.createPlaceholderThumbnail.bind(this); @@ -404,31 +407,23 @@ class ThumbnailsSidebar { return; } - if (!this.isOpen()) { + if (!this.isOpen) { this.toggleOpen(); } else { this.toggleClose(); } } - /** - * Returns whether the sidebar is open or not - * @return {boolean} true if the sidebar is open, false if not - */ - isOpen() { - return this.anchorEl && !this.anchorEl.classList.contains(CLASS_HIDDEN); - } - /** * Toggles the sidebar open. This will scroll the current page into view * @return {void} */ toggleOpen() { - if (!this.anchorEl) { + if (!this.virtualScroller) { return; } - this.anchorEl.classList.remove(CLASS_HIDDEN); + this.isOpen = true; this.virtualScroller.scrollIntoView(this.currentPage - 1); } @@ -438,11 +433,7 @@ class ThumbnailsSidebar { * @return {void} */ toggleClose() { - if (!this.anchorEl) { - return; - } - - this.anchorEl.classList.add(CLASS_HIDDEN); + this.isOpen = false; } /** diff --git a/src/lib/__tests__/ThumbnailsSidebar-test.js b/src/lib/__tests__/ThumbnailsSidebar-test.js index 0ef1478b4..684b3d652 100644 --- a/src/lib/__tests__/ThumbnailsSidebar-test.js +++ b/src/lib/__tests__/ThumbnailsSidebar-test.js @@ -1,7 +1,6 @@ /* eslint-disable no-unused-expressions */ import ThumbnailsSidebar from '../ThumbnailsSidebar'; import VirtualScroller from '../VirtualScroller'; -import { CLASS_HIDDEN } from '../constants'; const sandbox = sinon.sandbox.create(); @@ -431,7 +430,6 @@ describe('ThumbnailsSidebar', () => { describe('toggle()', () => { beforeEach(() => { - stubs.isOpen = sandbox.stub(thumbnailsSidebar, 'isOpen'); stubs.toggleOpen = sandbox.stub(thumbnailsSidebar, 'toggleOpen'); stubs.toggleClose = sandbox.stub(thumbnailsSidebar, 'toggleClose'); }); @@ -441,7 +439,6 @@ describe('ThumbnailsSidebar', () => { thumbnailsSidebar.toggle(); - expect(stubs.isOpen).not.to.be.called; expect(stubs.toggleOpen).not.to.be.called; expect(stubs.toggleClose).not.to.be.called; @@ -449,21 +446,19 @@ describe('ThumbnailsSidebar', () => { }); it('should toggle open if it was closed', () => { - stubs.isOpen.returns(false); + thumbnailsSidebar.isOpen = false; thumbnailsSidebar.toggle(); - expect(stubs.isOpen).to.be.called; expect(stubs.toggleOpen).to.be.called; expect(stubs.toggleClose).not.to.be.called; }); it('should toggle closed if it was open', () => { - stubs.isOpen.returns(true); + thumbnailsSidebar.isOpen = true; thumbnailsSidebar.toggle(); - expect(stubs.isOpen).to.be.called; expect(stubs.toggleOpen).not.to.be.called; expect(stubs.toggleClose).to.be.called; }); @@ -475,15 +470,14 @@ describe('ThumbnailsSidebar', () => { thumbnailsSidebar.virtualScroller = virtualScroller; }); - it('should do nothing if there is no anchorEl', () => { - thumbnailsSidebar.anchorEl = null; + it('should do nothing if there is no virtualScroller', () => { + thumbnailsSidebar.virtualScroller = null; + thumbnailsSidebar.isOpen = false; thumbnailsSidebar.toggleOpen(); - expect(stubs.removeClass).not.to.be.called; + expect(thumbnailsSidebar.isOpen).to.be.false; expect(stubs.vsScrollIntoView).not.to.be.called; - - thumbnailsSidebar.anchorEl = anchorEl; }); it('should remove the hidden class and scroll the page into view', () => { @@ -491,30 +485,18 @@ describe('ThumbnailsSidebar', () => { thumbnailsSidebar.toggleOpen(); - expect(stubs.removeClass).to.be.calledWith(CLASS_HIDDEN); + expect(thumbnailsSidebar.isOpen).to.be.true; expect(stubs.vsScrollIntoView).to.be.calledWith(2); }); }); describe('toggleClose()', () => { - beforeEach(() => { - stubs.addClass = sandbox.stub(thumbnailsSidebar.anchorEl.classList, 'add'); - }); - - it('should do nothing if there is no anchorEl', () => { - thumbnailsSidebar.anchorEl = null; - - thumbnailsSidebar.toggleClose(); - - expect(stubs.addClass).not.to.be.called; - - thumbnailsSidebar.anchorEl = anchorEl; - }); + it('should set isOpen to false', () => { + thumbnailsSidebar.isOpen = true; - it('should add the hidden class', () => { thumbnailsSidebar.toggleClose(); - expect(stubs.addClass).to.be.calledWith(CLASS_HIDDEN); + expect(thumbnailsSidebar.isOpen).to.be.false; }); }); }); diff --git a/src/lib/_common.scss b/src/lib/_common.scss index 300a04b8a..098b83318 100644 --- a/src/lib/_common.scss +++ b/src/lib/_common.scss @@ -147,10 +147,14 @@ $header-height: 48px; .bp-content { align-items: center; + bottom: 0; display: flex; flex: 1 1 auto; + left: 0; outline: none; - position: relative; + position: absolute; + right: 0; + top: 0; } .bp-content.bp-is-fullscreen { @@ -159,6 +163,10 @@ $header-height: 48px; } } +.bp.bp-loaded .bp-content { + transition: left 300ms cubic-bezier(.4, 0, .2, 1); +} + .accessibility-hidden { left: -9999px; position: absolute; diff --git a/src/lib/constants.js b/src/lib/constants.js index 42090a76a..8fc1f9005 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -35,6 +35,7 @@ export const CLASS_BOX_PREVIEW_NOTIFICATION = 'bp-notification'; export const CLASS_BOX_PREVIEW_NOTIFICATION_WRAPPER = 'bp-notifications-wrapper'; export const CLASS_BOX_PREVIEW_TOGGLE_OVERLAY = 'bp-toggle-overlay'; export const CLASS_BOX_PREVIEW_THEME_DARK = 'bp-theme-dark'; +export const CLASS_BOX_PREVIEW_THUMBNAILS_OPEN = 'bp-thumbnails-open'; export const CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER = 'bp-thumbnails-container'; export const CLASS_ELEM_KEYBOARD_FOCUS = 'bp-has-keyboard-focus'; export const CLASS_FULLSCREEN = 'bp-is-fullscreen'; diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index ecc8686e9..86b95bc35 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -21,7 +21,8 @@ import { QUERY_PARAM_ENCODING, ENCODING_TYPES, CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER, - ANNOTATOR_EVENT + ANNOTATOR_EVENT, + CLASS_BOX_PREVIEW_THUMBNAILS_OPEN } from '../../constants'; import { checkPermission, getRepresentation } from '../../file'; import { appendQueryParams, createAssetUrlCreator, getMidpoint, getDistance, getClosestPageToPinch } from '../../util'; @@ -39,24 +40,25 @@ import Timer from '../../Timer'; const CURRENT_PAGE_MAP_KEY = 'doc-current-page-map'; const DEFAULT_SCALE_DELTA = 1.1; +const IS_SAFARI_CLASS = 'is-safari'; const LOAD_TIMEOUT_MS = 180000; // 3 min timeout -const SAFARI_PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print -const PRINT_DIALOG_TIMEOUT_MS = 500; -const MAX_SCALE = 10.0; -const MIN_SCALE = 0.1; const MAX_PINCH_SCALE_VALUE = 3; -const MIN_PINCH_SCALE_VALUE = 0.25; +const MAX_SCALE = 10.0; const MIN_PINCH_SCALE_DELTA = 0.01; -const IS_SAFARI_CLASS = 'is-safari'; -const SCROLL_EVENT_THROTTLE_INTERVAL = 200; -const SCROLL_END_TIMEOUT = this.isMobile ? 500 : 250; -const RANGE_REQUEST_CHUNK_SIZE_US = 1048576; // 1MB -const RANGE_REQUEST_CHUNK_SIZE_NON_US = 524288; // 512KB +const MIN_PINCH_SCALE_VALUE = 0.25; +const MIN_SCALE = 0.1; const MINIMUM_RANGE_REQUEST_FILE_SIZE_NON_US = 26214400; // 25MB const MOBILE_MAX_CANVAS_SIZE = 2949120; // ~3MP 1920x1536 +const PAGES_UNIT_NAME = 'pages'; const PINCH_PAGE_CLASS = 'pinch-page'; const PINCHING_CLASS = 'pinching'; -const PAGES_UNIT_NAME = 'pages'; +const PRINT_DIALOG_TIMEOUT_MS = 500; +const RANGE_REQUEST_CHUNK_SIZE_NON_US = 524288; // 512KB +const RANGE_REQUEST_CHUNK_SIZE_US = 1048576; // 1MB +const SAFARI_PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print +const SCROLL_END_TIMEOUT = this.isMobile ? 500 : 250; +const SCROLL_EVENT_THROTTLE_INTERVAL = 200; +const THUMBNAILS_SIDEBAR_TRANSITION_TIME = 301; // 301ms // List of metrics to be emitted only once per session const METRICS_WHITELIST = [ USER_DOCUMENT_THUMBNAIL_EVENTS.CLOSE, @@ -127,7 +129,7 @@ class DocBaseViewer extends BaseViewer { if (this.options.enableThumbnailsSidebar) { this.thumbnailsSidebarEl = document.createElement('div'); - this.thumbnailsSidebarEl.className = `${CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER} ${CLASS_HIDDEN}`; + this.thumbnailsSidebarEl.className = `${CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER}`; this.thumbnailsSidebarEl.setAttribute('data-testid', 'thumbnails-sidebar'); this.containerEl.parentNode.insertBefore(this.thumbnailsSidebarEl, this.containerEl); } @@ -1329,10 +1331,12 @@ class DocBaseViewer extends BaseViewer { let metricName; let eventName; - if (!this.thumbnailsSidebar.isOpen()) { + if (!this.thumbnailsSidebar.isOpen) { + this.rootEl.classList.remove(CLASS_BOX_PREVIEW_THUMBNAILS_OPEN); metricName = USER_DOCUMENT_THUMBNAIL_EVENTS.CLOSE; eventName = 'thumbnailsClose'; } else { + this.rootEl.classList.add(CLASS_BOX_PREVIEW_THUMBNAILS_OPEN); metricName = USER_DOCUMENT_THUMBNAIL_EVENTS.OPEN; eventName = 'thumbnailsOpen'; } @@ -1340,7 +1344,8 @@ class DocBaseViewer extends BaseViewer { this.emitMetric({ name: metricName, data: pagesCount }); this.emit(eventName); - this.resize(); + // Resize after the CSS animation to toggle the sidebar is complete + setTimeout(() => this.resize(), THUMBNAILS_SIDEBAR_TRANSITION_TIME); } /** diff --git a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js index a27cb628f..d30fb1bd0 100644 --- a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js +++ b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js @@ -21,7 +21,8 @@ import { QUERY_PARAM_ENCODING, ENCODING_TYPES, SELECTOR_BOX_PREVIEW_CONTENT, - CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER + CLASS_BOX_PREVIEW_THUMBNAILS_CONTAINER, + CLASS_BOX_PREVIEW_THUMBNAILS_OPEN } from '../../../constants'; import { ICON_PRINT_CHECKMARK, @@ -72,6 +73,7 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { fixture.load('viewers/doc/__tests__/DocBaseViewer-test.html'); containerEl = document.querySelector(SELECTOR_BOX_PREVIEW_CONTENT); + stubs = {}; docBase = new DocBaseViewer({ cache: { set: () => {}, @@ -92,9 +94,19 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { enableThumbnailsSidebar: true }); Object.defineProperty(BaseViewer.prototype, 'setup', { value: sandbox.stub() }); + stubs.classListAdd = sandbox.stub(); + stubs.classListRemove = sandbox.stub(); + const rootEl = { + classList: { + add: stubs.classListAdd, + remove: stubs.classListRemove + } + }; + docBase.containerEl = containerEl; + docBase.rootEl = rootEl; + docBase.setup(); - stubs = {}; }); afterEach(() => { @@ -2212,26 +2224,34 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { describe('toggleThumbnails()', () => { let thumbnailsSidebar; + let clock; beforeEach(() => { sandbox.stub(docBase, 'resize'); sandbox.stub(docBase, 'emitMetric'); sandbox.stub(docBase, 'emit'); + clock = sinon.useFakeTimers(); + stubs.toggleSidebar = sandbox.stub(); stubs.isSidebarOpen = sandbox.stub(); thumbnailsSidebar = { toggle: stubs.toggleSidebar, - isOpen: stubs.isSidebarOpen, + isOpen: false, destroy: () => {} }; }); + afterEach(() => { + clock.restore(); + }); + it('should do nothing if thumbnails sidebar does not exist', () => { docBase.thumbnailsSidebar = undefined; docBase.toggleThumbnails(); + clock.tick(300); expect(docBase.resize).not.to.be.called; }); @@ -2239,12 +2259,13 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { it('should toggle open and resize the viewer', () => { docBase.thumbnailsSidebar = thumbnailsSidebar; docBase.pdfViewer = { pagesCount: 10 }; - stubs.isSidebarOpen.returns(true); + thumbnailsSidebar.isOpen = true; docBase.toggleThumbnails(); + clock.tick(301); + expect(stubs.classListAdd).to.be.calledWith(CLASS_BOX_PREVIEW_THUMBNAILS_OPEN); expect(stubs.toggleSidebar).to.be.called; - expect(stubs.isSidebarOpen).to.be.called; expect(docBase.resize).to.be.called; expect(docBase.emitMetric).to.be.calledWith({ name: USER_DOCUMENT_THUMBNAIL_EVENTS.OPEN, data: 10 }); expect(docBase.emit).to.be.calledWith('thumbnailsOpen'); @@ -2253,12 +2274,12 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { it('should toggle close and resize the viewer', () => { docBase.thumbnailsSidebar = thumbnailsSidebar; docBase.pdfViewer = { pagesCount: 10 }; - stubs.isSidebarOpen.returns(false); docBase.toggleThumbnails(); + clock.tick(301); + expect(stubs.classListRemove).to.be.calledWith(CLASS_BOX_PREVIEW_THUMBNAILS_OPEN); expect(stubs.toggleSidebar).to.be.called; - expect(stubs.isSidebarOpen).to.be.called; expect(docBase.resize).to.be.called; expect(docBase.emitMetric).to.be.calledWith({ name: USER_DOCUMENT_THUMBNAIL_EVENTS.CLOSE, data: 10 }); expect(docBase.emit).to.be.calledWith('thumbnailsClose'); diff --git a/src/lib/viewers/doc/_docBase.scss b/src/lib/viewers/doc/_docBase.scss index c2a566673..8d5a03b21 100644 --- a/src/lib/viewers/doc/_docBase.scss +++ b/src/lib/viewers/doc/_docBase.scss @@ -1,12 +1,31 @@ @import '../../boxuiVariables'; $thumbnail-border-radius: 4px; +// Accounts for the 1px border on the right so the remaining width is actually 200 +$thumbnail-sidebar-width: 201px; .bp { .bp-thumbnails-container { border-right: solid 1px $seesee; + bottom: 0; display: flex; - flex: 0 0 201px; // Accounts for the 1px border on the right so the remaining width is actually 180 + flex: 0 0 auto; + left: 0; + position: absolute; + top: 0; + transform: translateX(-$thumbnail-sidebar-width); + transition: transform 300ms cubic-bezier(.4, 0, .2, 1); + width: $thumbnail-sidebar-width; + } + + &.bp-thumbnails-open { + .bp-thumbnails-container { + transform: translateX(0); + } + + .bp-content { + left: $thumbnail-sidebar-width; + } } .bp-thumbnail { diff --git a/test/integration/document/Thumbnails.e2e.test.js b/test/integration/document/Thumbnails.e2e.test.js index eb130dcb4..a8b3e1b5f 100644 --- a/test/integration/document/Thumbnails.e2e.test.js +++ b/test/integration/document/Thumbnails.e2e.test.js @@ -44,9 +44,9 @@ describe('Preview Document Thumbnails', () => { it('Should render thumbnails when toggled', () => { showDocumentPreview({ enableThumbnailsSidebar: true }); - toggleThumbnails().should('be.visible'); + toggleThumbnails().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)'); // translateX(0) - toggleThumbnails().should('not.be.visible'); + toggleThumbnails().should('have.css', 'transform', 'matrix(1, 0, 0, 1, -201, 0)'); // translateX(-201px) }); it('Should be able to change page by clicking on the thumbnail', () => {