diff --git a/src/amo/components/ScreenShots.js b/src/amo/components/ScreenShots.js deleted file mode 100644 index 348593aa411..00000000000 --- a/src/amo/components/ScreenShots.js +++ /dev/null @@ -1,84 +0,0 @@ -/* global document, window */ -/* eslint-disable jsx-a11y/href-no-hash */ - -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { PhotoSwipeGallery } from 'react-photoswipe'; -import 'react-photoswipe/lib/photoswipe.css'; - -import 'amo/css/ScreenShots.scss'; - -const PHOTO_SWIPE_OPTIONS = { - closeEl: true, - captionEl: true, - fullscreenEl: false, - zoomEl: false, - shareEl: false, - counterEl: true, - arrowEl: true, - preloaderEl: true, - // Overload getThumbsBoundsFn as workaround to - // https://github.com/minhtranite/react-photoswipe/issues/23 - getThumbBoundsFn: /* istanbul ignore next */ function getThumbBoundsFn(index) { - const thumbnail = document.querySelectorAll('.pswp-thumbnails')[index]; - if (thumbnail && thumbnail.getElementsByTagName) { - const img = thumbnail.getElementsByTagName('img')[0]; - const pageYScroll = window.pageYOffset || document.documentElement.scrollTop; - const rect = img.getBoundingClientRect(); - return { x: rect.left, y: rect.top + pageYScroll, w: rect.width }; - } - return false; - }, -}; - -const formatPreviews = (previews) => ( - previews.map((preview) => ({ - src: preview.image_url, - thumbnail_src: preview.thumbnail_url, - h: preview.image_size[1], - w: preview.image_size[0], - title: preview.caption, - })) -); - -export const thumbnailContent = (item) => ( - {item.title} -); - -export default class ScreenShots extends React.Component { - static propTypes = { - previews: PropTypes.array.isRequired, - } - - onClose = (photoswipe) => { - const index = photoswipe.getCurrentIndex(); - const list = this.viewport.querySelector('.pswp-thumbnails'); - const currentItem = list.children[index]; - const offset = currentItem.getBoundingClientRect().x; - list.scrollLeft += offset - list.getBoundingClientRect().x; - } - - render() { - const { previews } = this.props; - return ( -
-
{ this.viewport = el; }}> - -
-
- ); - } -} diff --git a/src/amo/components/ScreenShots/index.js b/src/amo/components/ScreenShots/index.js new file mode 100644 index 00000000000..4adc0d6d5b8 --- /dev/null +++ b/src/amo/components/ScreenShots/index.js @@ -0,0 +1,140 @@ +/* @flow */ +/* global document, window */ +/* eslint-disable jsx-a11y/href-no-hash */ +import invariant from 'invariant'; +import * as React from 'react'; +import { PhotoSwipeGallery } from 'react-photoswipe'; +import 'react-photoswipe/lib/photoswipe.css'; + +import './styles.scss'; + +type ThumbBounds = false | {| + w: number, + x: number, + y: number, +|}; + +type GetThumbBoundsExtraParams = {| + _document: typeof document | null, + _window: typeof window | null, +|}; + +export const PHOTO_SWIPE_OPTIONS = { + closeEl: true, + captionEl: true, + fullscreenEl: false, + zoomEl: false, + shareEl: false, + counterEl: true, + arrowEl: true, + preloaderEl: true, + // Overload getThumbBoundsFn as workaround to + // https://github.com/minhtranite/react-photoswipe/issues/23 + getThumbBoundsFn: (index: number, { + // $FLOW_FIXME: see https://github.com/facebook/flow/issues/183 + _document = typeof document !== 'undefined' ? document : null, + _window = typeof window !== 'undefined' ? window : null, + }: GetThumbBoundsExtraParams = {}): ThumbBounds => { + if (!_document || !_window) { + return false; + } + + const thumbnail = _document.querySelectorAll('.pswp-thumbnails')[index]; + + if (thumbnail && thumbnail.getElementsByTagName) { + const img = thumbnail.getElementsByTagName('img')[0]; + const pageYScroll = _window.pageYOffset || ( + _document.documentElement ? _document.documentElement.scrollTop : 0 + ); + const rect = img.getBoundingClientRect(); + + return { x: rect.left, y: rect.top + pageYScroll, w: rect.width }; + } + + return false; + }, +}; + +type ExternalPreview = {| + caption: string, + image_size: [number, number], + image_url: string, + thumbnail_size: [number, number], + thumbnail_url: string, +|}; + +type Preview = {| + h: number, + src: string, + thumbnail_h: number, + thumbnail_src: string, + thumbnail_w: number, + title: string, + w: number, +|}; + +const formatPreviews = (previews: Array): Array => ( + previews.map((preview) => ({ + h: preview.image_size[1], + src: preview.image_url, + thumbnail_h: preview.thumbnail_size[1], + thumbnail_src: preview.thumbnail_url, + thumbnail_w: preview.thumbnail_size[0], + title: preview.caption, + w: preview.image_size[0], + })) +); + +export const thumbnailContent = (item: Preview): React.Node => ( + {item.title} +); + +type Props = {| + previews: Array, +|}; + +export default class ScreenShots extends React.Component { + onClose = (photoswipe: Object) => { + const index = photoswipe.getCurrentIndex(); + + invariant(this.viewport, 'viewport ref is required'); + + const list = this.viewport.querySelector('.pswp-thumbnails'); + + invariant(list, 'list is required'); + + const currentItem = list.children[index]; + const offset = currentItem.getBoundingClientRect().left; + list.scrollLeft += offset - list.getBoundingClientRect().left; + } + + viewport: HTMLElement | null; + + render() { + const { previews } = this.props; + + return ( +
+
{ this.viewport = el; }} + > + +
+
+ ); + } +} diff --git a/src/amo/css/ScreenShots.scss b/src/amo/components/ScreenShots/styles.scss similarity index 100% rename from src/amo/css/ScreenShots.scss rename to src/amo/components/ScreenShots/styles.scss diff --git a/tests/unit/amo/components/TestScreenShots.js b/tests/unit/amo/components/TestScreenShots.js index c97afa722e2..8eb45c35916 100644 --- a/tests/unit/amo/components/TestScreenShots.js +++ b/tests/unit/amo/components/TestScreenShots.js @@ -3,6 +3,7 @@ import * as React from 'react'; import { PhotoSwipeGallery } from 'react-photoswipe'; import ScreenShots, { + PHOTO_SWIPE_OPTIONS, thumbnailContent, } from 'amo/components/ScreenShots'; @@ -16,14 +17,14 @@ describe(__filename, () => { image_url: 'http://img.com/one', thumbnail_url: 'http://img.com/1', image_size: [WIDTH, HEIGHT], - thumbnail_size: [WIDTH, HEIGHT], + thumbnail_size: [WIDTH - 100, HEIGHT - 100], }, { caption: 'Another screenshot', image_url: 'http://img.com/two', thumbnail_url: 'http://img.com/2', image_size: [WIDTH, HEIGHT], - thumbnail_size: [WIDTH, HEIGHT], + thumbnail_size: [WIDTH - 100, HEIGHT - 100], }, ]; @@ -33,6 +34,8 @@ describe(__filename, () => { title: 'A screenshot', src: 'http://img.com/one', thumbnail_src: 'http://img.com/1', + thumbnail_w: WIDTH - 100, + thumbnail_h: HEIGHT - 100, h: HEIGHT, w: WIDTH, }, @@ -40,6 +43,8 @@ describe(__filename, () => { title: 'Another screenshot', src: 'http://img.com/two', thumbnail_src: 'http://img.com/2', + thumbnail_w: WIDTH - 100, + thumbnail_h: HEIGHT - 100, h: HEIGHT, w: WIDTH, }, @@ -53,16 +58,26 @@ describe(__filename, () => { }); it('renders custom thumbnail', () => { - const h = 123; - const w = 1234; + const thumbnailSrc = 'http://example.com/thumbnail.png'; + const thumbnailHeight = 123; + const thumbnailWidth = 200; + + const item = { + src: 'https://foo.com/img.png', + title: 'test title', + h: HEIGHT, + w: WIDTH, + thumbnail_src: thumbnailSrc, + thumbnail_h: thumbnailHeight, + thumbnail_w: thumbnailWidth, + }; - const item = { src: 'https://foo.com/img.png', title: 'test title', h, w }; const thumbnail = shallow(thumbnailContent(item)); expect(thumbnail.type()).toEqual('img'); - expect(thumbnail.prop('src')).toEqual('https://foo.com/img.png'); - expect(thumbnail.prop('height')).toEqual(h); - expect(thumbnail.prop('width')).toEqual(w); + expect(thumbnail.prop('src')).toEqual(thumbnailSrc); + expect(thumbnail.prop('height')).toEqual(thumbnailHeight); + expect(thumbnail.prop('width')).toEqual(thumbnailWidth); expect(thumbnail.prop('alt')).toEqual('test title'); expect(thumbnail.prop('title')).toEqual('test title'); }); @@ -74,16 +89,122 @@ describe(__filename, () => { )); const root = mount(); - const item = { getBoundingClientRect: () => ({ x: 500 }) }; + const item = { getBoundingClientRect: () => ({ left: 500 }) }; const list = { children: [null, item], - getBoundingClientRect: () => ({ x: 55 }), + getBoundingClientRect: () => ({ left: 55 }), scrollLeft: 0, }; sinon.stub(root.instance().viewport, 'querySelector').returns(list); + const photoswipe = { getCurrentIndex: () => 1 }; root.instance().onClose(photoswipe); // 0 += 500 - 55 expect(list.scrollLeft).toEqual(445); }); + + describe('PHOTO_SWIPE_OPTIONS.getThumbBoundsFn', () => { + const { getThumbBoundsFn } = PHOTO_SWIPE_OPTIONS; + + const getFakeDocument = ({ left, top, width }) => { + const fakeImg = { + getBoundingClientRect: () => ({ + height: 123, + left, + top, + width, + }), + }; + + const fakeThumbnail = { + getElementsByTagName: () => [fakeImg], + }; + + const fakeDocument = { + querySelectorAll: () => [fakeThumbnail], + }; + + return fakeDocument; + }; + + it('returns false if thumbnail does not exist', () => { + const bounds = getThumbBoundsFn(0); + + expect(bounds).toEqual(false); + }); + + it('returns false if _document is null', () => { + const bounds = getThumbBoundsFn(0, { _document: null }); + + expect(bounds).toEqual(false); + }); + + it('returns false if _window is null', () => { + const bounds = getThumbBoundsFn(0, { + _document: getFakeDocument({ left: 1, top: 2, width: 3 }), + _window: null, + }); + + expect(bounds).toEqual(false); + }); + + it('returns an object with x, y and w values', () => { + const left = 123; + const top = 124; + const width = 100; + + const fakeDocument = getFakeDocument({ left, top, width }); + + const bounds = getThumbBoundsFn(0, { _document: fakeDocument }); + + expect(bounds).toEqual({ + w: width, + x: left, + y: top, + }); + }); + + it('uses window.pageYOffset to compute `y` if available', () => { + const left = 123; + const top = 124; + const width = 100; + + const fakeDocument = getFakeDocument({ left, top, width }); + + const fakeWindow = { + pageYOffset: 20, + }; + + const bounds = getThumbBoundsFn(0, { + _document: fakeDocument, + _window: fakeWindow, + }); + + expect(bounds).toEqual({ + w: width, + x: left, + y: top + fakeWindow.pageYOffset, + }); + }); + + it('uses document.documentElement.scrollTop to compute `y` if available', () => { + const left = 123; + const top = 124; + const width = 100; + const scrollTop = 30; + + const fakeDocument = getFakeDocument({ left, top, width }); + fakeDocument.documentElement = { + scrollTop, + }; + + const bounds = getThumbBoundsFn(0, { _document: fakeDocument }); + + expect(bounds).toEqual({ + w: width, + x: left, + y: top + scrollTop, + }); + }); + }); });