aboutsummaryrefslogtreecommitdiff
path: root/src/components/popover/popover.js
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2022-07-31 11:44:15 +0300
committerHenry Jameson <me@hjkos.com>2022-07-31 11:44:15 +0300
commit1cf7af33741ee43aff9e597f0a69f62f7d660cf6 (patch)
tree75eea24b29b4204b57e38aec8223e91a1f1526fe /src/components/popover/popover.js
parent0b88c56aa674ad19be7e7e883a3687ec89569940 (diff)
parent36aae1635ad370ecf4d22ae6d62cbbba6af19fd3 (diff)
Merge branch 'disjointed-popovers' into eslint-update
* disjointed-popovers: (56 commits) fix typo fix errors in console pinned no longer needed popover stack add stay-on-click prop to solve case of clicking user avatar in status popover fix settings tooltips vertical nudge for popovers, especially for overlay-centers ones make user popover options expert use same sizing for timeline dropdown as in the main nav fix avatar not zooming in profile page fix spacing in mentionsline add popovers to chats fix avatar not closing, add option to put popovers next to avatar instead of over it fix the incorrect rounding in nav list re-unfuck the timeline popover Revert "unify styling of timelines dropdown with other dropdown menus" close on avatar click again, add zooming as option fix basicusercard make hover popovers less annoying to close move tooltips setting ...
Diffstat (limited to 'src/components/popover/popover.js')
-rw-r--r--src/components/popover/popover.js221
1 files changed, 173 insertions, 48 deletions
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index a30a37c9..58f7126d 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -31,13 +31,35 @@ const Popover = {
// If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom.
- removePadding: Boolean
+ removePadding: Boolean,
+
+ // self-explanatory (i hope)
+ disabled: Boolean,
+
+ // Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
+ overlayCenters: Boolean,
+
+ // What selector (witin popover!) to use for determining center of popover
+ overlayCentersSelector: String,
+
+ // Lets hover popover stay when clicking inside of it
+ stayOnClick: Boolean
},
+ inject: ['popoversZLayer'], // override popover z layer
data () {
return {
+ // lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
+ // so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
+ // with popovers refusing to be hidden when user wants to interact with something in below popover
+ lockReEntry: false,
hidden: true,
- styles: { opacity: 0 },
- oldSize: { width: 0, height: 0 }
+ styles: {},
+ oldSize: { width: 0, height: 0 },
+ scrollable: null,
+ // used to avoid blinking if hovered onto popover
+ graceTimeout: null,
+ parentPopover: null,
+ childrenShown: new Set()
}
},
methods: {
@@ -47,9 +69,7 @@ const Popover = {
},
updateStyles () {
if (this.hidden) {
- this.styles = {
- opacity: 0
- }
+ this.styles = {}
return
}
@@ -57,14 +77,26 @@ const Popover = {
// its children are what are inside the slot. Expect only one v-slot:trigger.
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback
- const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
- const screenBox = anchorEl.getBoundingClientRect()
- // Screen position of the origin point for popover
- const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
+ const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
+ const anchorScreenBox = anchorEl.getBoundingClientRect()
+
+ const anchorStyle = getComputedStyle(anchorEl)
+ const topPadding = parseFloat(anchorStyle.paddingTop)
+ const bottomPadding = parseFloat(anchorStyle.paddingBottom)
+
+ // Screen position of the origin point for popover = center of the anchor
+ const origin = {
+ x: anchorScreenBox.left + anchorWidth * 0.5,
+ y: anchorScreenBox.top + anchorHeight * 0.5
+ }
const content = this.$refs.content
+ const overlayCenter = this.overlayCenters
+ ? this.$refs.content.querySelector(this.overlayCentersSelector)
+ : null
+
// Minor optimization, don't call a slow reflow call if we don't have to
- const parentBounds = this.boundTo &&
+ const parentScreenBox = this.boundTo &&
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.containerBoundingClientRect()
@@ -73,81 +105,151 @@ const Popover = {
// What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default margin values to dodge the navbar
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
- min: parentBounds.left + (margin.left || 0),
- max: parentBounds.right - (margin.right || 0)
+ min: parentScreenBox.left + (margin.left || 0),
+ max: parentScreenBox.right - (margin.right || 0)
} : {
min: 0 + (margin.left || 10),
max: window.innerWidth - (margin.right || 10)
}
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
- min: parentBounds.top + (margin.top || 0),
- max: parentBounds.bottom - (margin.bottom || 0)
+ min: parentScreenBox.top + (margin.top || 0),
+ max: parentScreenBox.bottom - (margin.bottom || 0)
} : {
min: 0 + (margin.top || 50),
max: window.innerHeight - (margin.bottom || 5)
}
let horizOffset = 0
+ let vertOffset = 0
+
+ if (overlayCenter) {
+ const box = content.getBoundingClientRect()
+ const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
+ const leftInnerOffset = overlayCenterScreenBox.left - box.left
+ const topInnerOffset = overlayCenterScreenBox.top - box.top
+ horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
+ vertOffset = -topInnerOffset - overlayCenter.offsetHeight * 0.5
+ } else {
+ horizOffset = content.offsetWidth * -0.5
+ vertOffset = content.offsetHeight * -0.5
+ }
+
+ const leftBorder = origin.x + horizOffset
+ const rightBorder = leftBorder + content.offsetWidth
+ const topBorder = origin.y + vertOffset
+ const bottomBorder = topBorder + content.offsetHeight
// If overflowing from left, move it so that it doesn't
- if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
- horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
+ if (leftBorder < xBounds.min) {
+ horizOffset += xBounds.min - leftBorder
}
// If overflowing from right, move it so that it doesn't
- if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
- horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
+ if (rightBorder > xBounds.max) {
+ horizOffset -= rightBorder - xBounds.max
}
- // Default to whatever user wished with placement prop
- let usingTop = this.placement !== 'bottom'
-
- // Handle special cases, first force to displaying on top if there's not space on bottom,
- // regardless of what placement value was. Then check if there's not space on top, and
- // force to bottom, again regardless of what placement value was.
- if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
- if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+ // If overflowing from top, move it so that it doesn't
+ if (topBorder < yBounds.min) {
+ vertOffset += yBounds.min - topBorder
+ }
- let vPadding = 0
- if (this.removePadding && usingTop) {
- const anchorStyle = getComputedStyle(anchorEl)
- vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
+ // If overflowing from bottom, move it so that it doesn't
+ if (bottomBorder > yBounds.max) {
+ vertOffset -= bottomBorder - yBounds.max
}
- const yOffset = (this.offset && this.offset.y) || 0
- const translateY = usingTop
- ? -anchorHeight + vPadding - yOffset - content.offsetHeight
- : yOffset
+ let translateX = 0
+ let translateY = 0
+
+ if (overlayCenter) {
+ translateX = origin.x + horizOffset
+ translateY = origin.y + vertOffset
+ } else {
+ // Default to whatever user wished with placement prop
+ let usingTop = this.placement !== 'bottom'
+
+ // Handle special cases, first force to displaying on top if there's not space on bottom,
+ // regardless of what placement value was. Then check if there's not space on top, and
+ // force to bottom, again regardless of what placement value was.
+ const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
+ const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
+ if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
+ if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
- const xOffset = (this.offset && this.offset.x) || 0
- const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
+ const yOffset = (this.offset && this.offset.y) || 0
+ translateY = usingTop
+ ? topBoundary - yOffset - content.offsetHeight
+ : bottomBoundary + yOffset
+
+ const xOffset = (this.offset && this.offset.x) || 0
+ translateX = origin.x + horizOffset + xOffset
+ }
- // Note, separate translateX and translateY avoids blurry text on chromium,
- // single translate or translate3d resulted in blurry text.
this.styles = {
- opacity: 1,
- transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
+ left: `${Math.round(translateX)}px`,
+ top: `${Math.round(translateY)}px`
+ }
+
+ if (this.popoversZLayer) {
+ this.styles['--ZI_popover_override'] = `var(--ZI_${this.popoversZLayer}_popovers)`
+ }
+ if (parentScreenBox) {
+ this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
}
},
showPopover () {
+ if (this.disabled) return
const wasHidden = this.hidden
this.hidden = false
+ this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
+ if (this.trigger === 'click' || this.stayOnClick) {
+ document.addEventListener('click', this.onClickOutside)
+ }
+ this.scrollable.addEventListener('scroll', this.onScroll)
+ this.scrollable.addEventListener('resize', this.onResize)
this.$nextTick(() => {
if (wasHidden) this.$emit('show')
this.updateStyles()
})
},
hidePopover () {
+ if (this.disabled) return
if (!this.hidden) this.$emit('close')
this.hidden = true
- this.styles = { opacity: 0 }
+ this.parentPopover && this.parentPopover.onChildPopoverState(this, false)
+ if (this.trigger === 'click') {
+ document.removeEventListener('click', this.onClickOutside)
+ }
+ this.scrollable.removeEventListener('scroll', this.onScroll)
+ this.scrollable.removeEventListener('resize', this.onResize)
},
onMouseenter (e) {
- if (this.trigger === 'hover') this.showPopover()
+ if (this.trigger === 'hover') {
+ this.lockReEntry = false
+ clearTimeout(this.graceTimeout)
+ this.graceTimeout = null
+ this.showPopover()
+ }
},
onMouseleave (e) {
- if (this.trigger === 'hover') this.hidePopover()
+ if (this.trigger === 'hover' && this.childrenShown.size === 0) {
+ this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+ }
+ },
+ onMouseenterContent (e) {
+ if (this.trigger === 'hover' && !this.lockReEntry) {
+ this.lockReEntry = true
+ clearTimeout(this.graceTimeout)
+ this.graceTimeout = null
+ this.showPopover()
+ }
+ },
+ onMouseleaveContent (e) {
+ if (this.trigger === 'hover' && this.childrenShown.size === 0) {
+ this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+ }
},
onClick (e) {
if (this.trigger === 'click') {
@@ -160,8 +262,24 @@ const Popover = {
},
onClickOutside (e) {
if (this.hidden) return
+ if (this.$refs.content && this.$refs.content.contains(e.target)) return
if (this.$el.contains(e.target)) return
+ if (this.childrenShown.size > 0) return
this.hidePopover()
+ if (this.parentPopover) this.parentPopover.onClickOutside(e)
+ },
+ onScroll (e) {
+ this.updateStyles()
+ },
+ onResize (e) {
+ this.updateStyles()
+ },
+ onChildPopoverState (childRef, state) {
+ if (state) {
+ this.childrenShown.add(childRef)
+ } else {
+ this.childrenShown.delete(childRef)
+ }
}
},
updated () {
@@ -175,11 +293,18 @@ const Popover = {
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
}
},
- created () {
- document.addEventListener('click', this.onClickOutside)
+ mounted () {
+ let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
+ this.$refs.trigger.closest('.mobile-notifications')
+ if (!scrollable) scrollable = window
+ this.scrollable = scrollable
+ let parent = this.$parent
+ while (parent && parent.$.type.name !== 'Popover') {
+ parent = parent.$parent
+ }
+ this.parentPopover = parent
},
- unmounted () {
- document.removeEventListener('click', this.onClickOutside)
+ beforeUnmount () {
this.hidePopover()
}
}