diff --git a/packages/ui/src/ocelot/icons/__screenshots__/chromium/all-icons.png b/packages/ui/src/ocelot/icons/__screenshots__/chromium/all-icons.png index 76caed616..aa060ba56 100644 Binary files a/packages/ui/src/ocelot/icons/__screenshots__/chromium/all-icons.png and b/packages/ui/src/ocelot/icons/__screenshots__/chromium/all-icons.png differ diff --git a/packages/ui/src/ocelot/icons/svgs/map-pin-filled.svg b/packages/ui/src/ocelot/icons/svgs/map-pin-filled.svg new file mode 100644 index 000000000..c8f6fa0ad --- /dev/null +++ b/packages/ui/src/ocelot/icons/svgs/map-pin-filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index 7970bc94b..a6d829d89 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -140,7 +140,7 @@ hr { transition-delay: 0; transition: opacity 80ms ease-out; - @media(hover: none) { + @media(pointer: coarse) { pointer-events: all; } } diff --git a/webapp/components/Dropdown.vue b/webapp/components/Dropdown.vue index 389e7ced4..39396df7d 100644 --- a/webapp/components/Dropdown.vue +++ b/webapp/components/Dropdown.vue @@ -48,8 +48,10 @@ export default { if (typeof document === 'undefined') return if (isOpen) { document.body.classList.add('dropdown-open') + this.addOverlayClickHandler() } else { document.body.classList.remove('dropdown-open') + this.removeOverlayClickHandler() } }, }, @@ -57,6 +59,7 @@ export default { beforeDestroy() { clearTimeout(mouseEnterTimer) clearTimeout(mouseLeaveTimer) + this.removeOverlayClickHandler() if (this.isPopoverOpen) { this.isPopoverOpen = false if (typeof document !== 'undefined') { @@ -116,6 +119,28 @@ export default { clearTimeout(mouseEnterTimer) clearTimeout(mouseLeaveTimer) }, + addOverlayClickHandler() { + this.overlayClickHandler = (e) => { + // Allow clicks inside the popover content (rendered under by v-tooltip) + if (e.target.closest('.tooltip-inner, .popover-inner')) return + // Allow clicks on the trigger itself + if (this.$el.contains(e.target)) return + e.stopPropagation() + e.preventDefault() + this.isPopoverOpen = false + } + // Capture phase fires before any other handler + setTimeout(() => { + if (this.isPopoverOpen && this.overlayClickHandler) { + document.addEventListener('click', this.overlayClickHandler, true) + } + }, 0) + }, + removeOverlayClickHandler() { + if (!this.overlayClickHandler) return + document.removeEventListener('click', this.overlayClickHandler, true) + this.overlayClickHandler = null + }, }, } diff --git a/webapp/components/UserTeaser/UserTeaser.spec.js b/webapp/components/UserTeaser/UserTeaser.spec.js index 3e6e6d127..055948ca8 100644 --- a/webapp/components/UserTeaser/UserTeaser.spec.js +++ b/webapp/components/UserTeaser/UserTeaser.spec.js @@ -5,6 +5,12 @@ import Vuex from 'vuex' const localVue = global.localVue +window.matchMedia = jest.fn().mockImplementation(() => ({ + matches: false, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), +})) + // Mock Math.random, used in Dropdown Object.assign(Math, { random: () => 0, @@ -12,11 +18,6 @@ Object.assign(Math, { const waitForPopover = async () => await new Promise((resolve) => setTimeout(resolve, 1000)) -let mockIsTouchDevice -jest.mock('../utils/isTouchDevice', () => ({ - isTouchDevice: jest.fn(() => mockIsTouchDevice), -})) - const userTilda = { name: 'Tilda Swinton', slug: 'tilda-swinton', @@ -65,7 +66,11 @@ describe('UserTeaser', () => { user = userTilda, withPopoverEnabled = true, }) => { - mockIsTouchDevice = onTouchScreen + window.matchMedia.mockImplementation(() => ({ + matches: onTouchScreen, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })) const store = new Vuex.Store({ getters: { diff --git a/webapp/components/UserTeaser/UserTeaserHelper.spec.js b/webapp/components/UserTeaser/UserTeaserHelper.spec.js index 49f52343a..26d7cd171 100644 --- a/webapp/components/UserTeaser/UserTeaserHelper.spec.js +++ b/webapp/components/UserTeaser/UserTeaserHelper.spec.js @@ -2,6 +2,16 @@ import { render, waitFor, fireEvent } from '@testing-library/vue' import { RouterLinkStub } from '@vue/test-utils' import UserTeaserHelper from './UserTeaserHelper.vue' +let mockMatchMediaMatches = false +const mockMatchMediaListeners = [] +window.matchMedia = jest.fn().mockImplementation(() => ({ + get matches() { + return mockMatchMediaMatches + }, + addEventListener: jest.fn((event, handler) => mockMatchMediaListeners.push(handler)), + removeEventListener: jest.fn(), +})) + const localVue = global.localVue const userLink = { @@ -9,12 +19,6 @@ const userLink = { params: { slug: 'slug', id: 'id' }, } -let mockIsTouchDevice - -jest.mock('../utils/isTouchDevice', () => ({ - isTouchDevice: jest.fn(() => mockIsTouchDevice), -})) - describe('UserTeaserHelper', () => { const Wrapper = ({ withLinkToProfile = true, @@ -22,7 +26,7 @@ describe('UserTeaserHelper', () => { withPopoverEnabled = true, hoverDelay = 500, }) => { - mockIsTouchDevice = onTouchScreen + mockMatchMediaMatches = onTouchScreen return render(UserTeaserHelper, { localVue, diff --git a/webapp/components/UserTeaser/UserTeaserHelper.vue b/webapp/components/UserTeaser/UserTeaserHelper.vue index 0194eed1f..67665561f 100644 --- a/webapp/components/UserTeaser/UserTeaserHelper.vue +++ b/webapp/components/UserTeaser/UserTeaserHelper.vue @@ -20,10 +20,11 @@