Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions app/assets/stylesheets/components/topics.css
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,6 @@ a.topic-icon {
}
}

.topic-row.is-popover-open {
position: relative;
z-index: 5;
}

.topic-row.topic-new,
.topic-row.topic-reading {
background: var(--color-bg-unread);
Expand Down
86 changes: 76 additions & 10 deletions app/javascript/controllers/hover_popover_controller.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
import { Controller } from "@hotwired/stimulus"

// Adds a small delay before hiding popovers so users can move the cursor into them.
//
// The popover is moved to <body> and pinned with `position: fixed` while open,
// instead of staying absolutely positioned inside the table row. Safari ignores
// z-index on <tr> elements once the table uses border-collapse: collapse, so a
// popover left inside the row rendered underneath the rows below it.
export default class extends Controller {
static targets = ["popover"]
static values = { delay: Number }

connect() {
this.hideTimeout = null
this.delay = this.delayValue || 150
this.homeParent = null
this.homeNextSibling = null
this.popover = this.hasPopoverTarget ? this.popoverTarget : null
this.onScroll = () => this._closeImmediately()

if (this.popover) {
this.onPopoverEnter = () => this.show()
this.onPopoverLeave = () => this.scheduleHide()
this.popover.addEventListener("mouseenter", this.onPopoverEnter)
this.popover.addEventListener("mouseleave", this.onPopoverLeave)
}
}

disconnect() {
this._clearTimeout()
if (this.popover) {
this.popover.removeEventListener("mouseenter", this.onPopoverEnter)
this.popover.removeEventListener("mouseleave", this.onPopoverLeave)
}
this._returnPopoverHome()
}

show() {
this._clearTimeout()
this.element.classList.add("is-open")
this._updateRowOpenState()
this._detachPopoverToBody()
}

scheduleHide() {
this._clearTimeout()
this.hideTimeout = setTimeout(() => {
this.element.classList.remove("is-open")
this._updateRowOpenState()
}, this.delay)
this.hideTimeout = setTimeout(() => this._closeImmediately(), this.delay)
}

_closeImmediately() {
this.element.classList.remove("is-open")
this._returnPopoverHome()
}

_clearTimeout() {
Expand All @@ -31,11 +58,50 @@ export default class extends Controller {
}
}

_updateRowOpenState() {
const row = this.element.closest(".topic-row")
if (!row) return
_detachPopoverToBody() {
if (!this.popover || this.popover.parentNode === document.body) return

this.homeParent = this.popover.parentNode
this.homeNextSibling = this.popover.nextSibling

document.body.appendChild(this.popover)
this.popover.style.display = "block"
this._positionPopover()

window.addEventListener("scroll", this.onScroll, { capture: true, passive: true })
window.addEventListener("resize", this.onScroll)
}

_positionPopover() {
const alignRight = this.element.closest(".topic-participants") !== null
const rect = this.element.getBoundingClientRect()

this.popover.style.position = "fixed"
this.popover.style.top = `${rect.bottom + 6}px`

if (alignRight) {
this.popover.style.left = "auto"
this.popover.style.right = `${window.innerWidth - rect.right}px`
} else {
this.popover.style.left = `${rect.left}px`
this.popover.style.right = "auto"
}
}

_returnPopoverHome() {
if (!this.popover || !this.homeParent) return

window.removeEventListener("scroll", this.onScroll, { capture: true })
window.removeEventListener("resize", this.onScroll)

this.popover.style.display = ""
this.popover.style.position = ""
this.popover.style.top = ""
this.popover.style.left = ""
this.popover.style.right = ""

const hasOpenPopover = row.querySelector(".topic-icon.is-open")
row.classList.toggle("is-popover-open", Boolean(hasOpenPopover))
this.homeParent.insertBefore(this.popover, this.homeNextSibling)
this.homeParent = null
this.homeNextSibling = null
}
}
Loading