diff --git a/src/components/Navigator/QuickNavigationModal.vue b/src/components/Navigator/QuickNavigationModal.vue index a7c71efbb..3b02059b0 100644 --- a/src/components/Navigator/QuickNavigationModal.vue +++ b/src/components/Navigator/QuickNavigationModal.vue @@ -60,7 +60,10 @@

@@ -141,6 +145,7 @@ import keyboardNavigation from 'docc-render/mixins/keyboardNavigation'; import LRUMap from 'docc-render/utils/lru-map'; import { convertChildrenArrayToObject, getParents } from 'docc-render/utils/navigatorData'; import { fetchDataForPreview } from 'docc-render/utils/data'; +import { SCROLL_LOCK_DISABLE_ATTR } from 'docc-render/utils/scroll-lock'; const { PreviewState } = QuickNavigationPreview.constants; @@ -174,6 +179,7 @@ export default { focusedInput: false, cachedSymbolResults: {}, previewIsLoadingSlowly: false, + SCROLL_LOCK_DISABLE_ATTR, }; }, props: { diff --git a/src/utils/scroll-lock.js b/src/utils/scroll-lock.js index 7b42f3e3f..a9ac31acd 100644 --- a/src/utils/scroll-lock.js +++ b/src/utils/scroll-lock.js @@ -11,6 +11,8 @@ let isLocked = false; let initialClientY = -1; let scrolledClientY = 0; +// Adds this attribute to an inner scrollable element to allow it to scroll +export const SCROLL_LOCK_DISABLE_ATTR = 'data-scroll-lock-disable'; const isIosDevice = () => window.navigator && window.navigator.platform @@ -79,12 +81,14 @@ function advancedUnlock(targetElement) { */ function handleScroll(event, targetElement) { const clientY = event.targetTouches[0].clientY - initialClientY; - if (targetElement.scrollTop === 0 && clientY > 0) { + // check if any parent has a scroll-lock disable, if not use the targetElement + const target = event.target.closest(`[${SCROLL_LOCK_DISABLE_ATTR}]`) || targetElement; + if (target.scrollTop === 0 && clientY > 0) { // element is at the top of its scroll. return preventDefault(event); } - if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { + if (isTargetElementTotallyScrolled(target) && clientY < 0) { // element is at the bottom of its scroll. return preventDefault(event); } diff --git a/tests/unit/components/Navigator/QuickNavigationModal.spec.js b/tests/unit/components/Navigator/QuickNavigationModal.spec.js index 22e98f930..25ab2c91d 100644 --- a/tests/unit/components/Navigator/QuickNavigationModal.spec.js +++ b/tests/unit/components/Navigator/QuickNavigationModal.spec.js @@ -16,6 +16,7 @@ import QuickNavigationHighlighter from '@/components/Navigator/QuickNavigationHi import QuickNavigationModal from '@/components/Navigator/QuickNavigationModal.vue'; import QuickNavigationPreview from '@/components/Navigator/QuickNavigationPreview.vue'; import Reference from '@/components/ContentNode/Reference.vue'; +import { SCROLL_LOCK_DISABLE_ATTR } from '@/utils/scroll-lock'; import { flushPromises } from '../../../../test-utils'; jest.mock('@/utils/data'); @@ -165,6 +166,7 @@ describe('QuickNavigationModal', () => { expect(wrapper.vm.debouncedInput).toBe(inputValue); expect(wrapper.findAll('.quick-navigation__symbol-match').length).toBe(filteredSymbols.length); expect(wrapper.find('.no-results').exists()).toBe(false); + expect(wrapper.find('.quick-navigation__refs').attributes(SCROLL_LOCK_DISABLE_ATTR)).toBeTruthy(); }); it('it renders the `no results found` string when no symbols are found given an input', () => { @@ -375,6 +377,7 @@ describe('QuickNavigationModal', () => { const preview = wrapper.find(QuickNavigationPreview); expect(preview.exists()).toBe(true); expect(preview.props('state')).toBe(PreviewState.loading); + expect(preview.attributes(SCROLL_LOCK_DISABLE_ATTR)).toBeTruthy(); }); it('renders with a successful state and data when data is loaded', async () => { diff --git a/tests/unit/utils/scroll-lock.spec.js b/tests/unit/utils/scroll-lock.spec.js index bdf15b481..3a67eede6 100644 --- a/tests/unit/utils/scroll-lock.spec.js +++ b/tests/unit/utils/scroll-lock.spec.js @@ -8,7 +8,7 @@ * See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import scrollLock from 'docc-render/utils/scroll-lock'; +import scrollLock, { SCROLL_LOCK_DISABLE_ATTR } from 'docc-render/utils/scroll-lock'; import { createEvent, parseHTMLString } from '../../../test-utils'; const { platform } = window.navigator; @@ -56,6 +56,9 @@ describe('scroll-lock', () => { preventDefault, stopPropagation, touches: [1], + target: { + closest: jest.fn(), + }, }; // init the scroll lock scrollLock.lockScroll(container); @@ -67,6 +70,8 @@ describe('scroll-lock', () => { container.ontouchmove(touchMoveEvent); expect(preventDefault).toHaveBeenCalledTimes(1); expect(stopPropagation).toHaveBeenCalledTimes(0); + expect(touchMoveEvent.target.closest).toHaveBeenCalledTimes(1); + expect(touchMoveEvent.target.closest).toHaveBeenCalledWith(`[${SCROLL_LOCK_DISABLE_ATTR}]`); // simulate scroll middle // simulate we have enough to scroll @@ -81,6 +86,21 @@ describe('scroll-lock', () => { expect(preventDefault).toHaveBeenCalledTimes(2); expect(stopPropagation).toHaveBeenCalledTimes(1); + // simulate there is a scroll-lock-disable target + container.ontouchmove({ + ...touchMoveEvent, + targetTouches: [{ clientY: -10 }], + target: { + closest: jest.fn().mockReturnValue({ + ...container, + clientHeight: 150, + }), + }, + }); + // assert scrolling was allowed + expect(preventDefault).toHaveBeenCalledTimes(2); + expect(stopPropagation).toHaveBeenCalledTimes(2); + scrollLock.unlockScroll(container); expect(container.ontouchmove).toBeFalsy(); expect(container.ontouchstart).toBeFalsy();