diff --git a/src/ViewerCore.tsx b/src/ViewerCore.tsx
index c055ec5d..edcb2804 100644
--- a/src/ViewerCore.tsx
+++ b/src/ViewerCore.tsx
@@ -10,8 +10,6 @@ import classnames from 'classnames';
function noop() { }
-// const transitionDuration = 300;
-
const ACTION_TYPES = {
setVisible: 'setVisible',
setActiveIndex: 'setActiveIndex',
@@ -173,6 +171,28 @@ export default (props: ViewerProps) => {
}
}, [visible]);
+ React.useEffect(() => {
+ if (visible || !state.visible) {
+ return;
+ }
+ // A disabled or interrupted transition may never emit transitionend.
+ const style = window.getComputedStyle(viewerCore.current);
+ const toMilliseconds = (value: string) => (parseFloat(value) || 0) * (value.trim().endsWith('ms') ? 1 : 1000);
+ const durations = style.getPropertyValue('transition-duration').split(',').map(toMilliseconds);
+ const delays = style.getPropertyValue('transition-delay').split(',').map(toMilliseconds);
+ const properties = style.getPropertyValue('transition-property').split(',');
+ const timeout = properties.reduce((longest, property, index) => {
+ if (property.trim() !== 'opacity' && property.trim() !== 'all') {
+ return longest;
+ }
+ return Math.max(longest, durations[index % durations.length] + delays[index % delays.length]);
+ }, 0);
+ const timer = window.setTimeout(() => {
+ dispatch(createAction(ACTION_TYPES.setVisible, { visible: false }));
+ }, timeout + 50);
+ return () => window.clearTimeout(timer);
+ }, [visible, state.visible]);
+
React.useEffect(() => {
bindEvent();
@@ -642,6 +662,7 @@ export default (props: ViewerProps) => {
let viewerStryle: React.CSSProperties = {
opacity: (visible && state.visible) ? 1 : 0,
display: (visible || state.visible) ? 'block' : 'none',
+ pointerEvents: visible ? undefined : 'none',
};
let activeImg: ImageDecorator = {
@@ -657,8 +678,8 @@ export default (props: ViewerProps) => {
{
- if (!visible) {
+ onTransitionEnd={(event) => {
+ if (!visible && event.target === event.currentTarget && event.propertyName === 'opacity') {
dispatch(createAction(ACTION_TYPES.setVisible, {
visible: false,
}));
diff --git a/src/__tests__/viewer.test.tsx b/src/__tests__/viewer.test.tsx
index cfbb76f6..55ee19a7 100644
--- a/src/__tests__/viewer.test.tsx
+++ b/src/__tests__/viewer.test.tsx
@@ -4,6 +4,7 @@ import ViewerProps from '../ViewerProps';
import { configure, mount } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import * as React from 'react';
+import { act } from 'react-dom/test-utils';
const img2 = require('../../demo/images/landscape2.jpg');
const img = require('../../demo/images/landscape.jpg');
@@ -241,6 +242,71 @@ describe('Viewer', () => {
expect($$('.react-viewer')[0].style.display).toBe('none');
});
+ it('hides the closed viewer when no transitionend event arrives', () => {
+ viewerHelper.new();
+ viewerHelper.open();
+ wrapper.find('.react-viewer-close').simulate('click');
+ viewerHelper.skipAnimation();
+ expect($$('.react-viewer')[0].style.display).toBe('none');
+ });
+
+ it('does not let an old close timer hide a reopened viewer', () => {
+ viewerHelper.new();
+ viewerHelper.open();
+ wrapper.find('.react-viewer-close').simulate('click');
+ wrapper.find('#viewer-tester-open-btn').simulate('click');
+ viewerHelper.skipAnimation();
+ expect($$('.react-viewer')[0].style.display).toBe('block');
+ expect($$('.react-viewer')[0].style.opacity).toBe('1');
+ });
+
+ it('waits for the configured opacity transition before fallback cleanup', () => {
+ viewerHelper.new();
+ viewerHelper.open();
+ const viewer = $$('.react-viewer')[0] as HTMLElement;
+ // This legacy jsdom does not implement CSS transition properties.
+ const computedStyle = window.getComputedStyle;
+ const styleSpy = jest.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
+ if (element === viewer) {
+ return { getPropertyValue: (property) => ({
+ 'transition-property': 'transform, opacity',
+ 'transition-duration': '100ms, 0.6s',
+ 'transition-delay': '0.1s',
+ }[property] || '') } as CSSStyleDeclaration;
+ }
+ return computedStyle(element);
+ });
+ try {
+ wrapper.find('.react-viewer-close').simulate('click');
+ } finally {
+ styleSpy.mockRestore();
+ }
+ expect(viewer.style.pointerEvents).toBe('none');
+ act(() => { jest.advanceTimersByTime(700); });
+ expect(viewer.style.display).toBe('block');
+ act(() => { jest.advanceTimersByTime(50); });
+ expect(viewer.style.display).toBe('none');
+ });
+
+ it('only finishes closing for the root opacity transition', () => {
+ viewerHelper.new();
+ viewerHelper.open();
+ const viewer = $$('.react-viewer')[0] as HTMLElement;
+ wrapper.find('.react-viewer-close').simulate('click');
+ wrapper.find('.react-viewer').simulate('transitionend', {
+ target: $$('.react-viewer-footer')[0], currentTarget: viewer, propertyName: 'opacity',
+ });
+ expect(viewer.style.display).toBe('block');
+ wrapper.find('.react-viewer').simulate('transitionend', {
+ target: viewer, currentTarget: viewer, propertyName: 'transform',
+ });
+ expect(viewer.style.display).toBe('block');
+ wrapper.find('.react-viewer').simulate('transitionend', {
+ target: viewer, currentTarget: viewer, propertyName: 'opacity',
+ });
+ expect(viewer.style.display).toBe('none');
+ });
+
it('exposes viewer controls to keyboard users', () => {
viewerHelper.new();
viewerHelper.open();