Files
livewire-material/resources/js/app-bar.js
T
Andreas Reinhold / reiniandClaude Opus 5 65788d0c61 Keep a date picker's and an app bar's settings on their own component
materialDatepicker set its first day, format, min, max and year range in
init() without declaring them, and materialAppBar did the same with its
resize observer and scroll handler. Alpine writes an undeclared property to
the outermost x-data scope, so two pickers inside one page scope (ReStride's
plan setup wraps its form in x-data) shared the last one's min and first day,
and a second app bar would have taken the first one's observer. Both now
declare them; a browser test puts two pickers in an outer scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
2026-09-13 19:17:11 +02:00

65 lines
2.2 KiB
JavaScript

/**
* `materialAppBar`: says when content is under `<x-app-bar>` (`scrolled`) and, for a medium or
* large bar, when it has collapsed to its row (`collapsed`). The collapsing itself is CSS
* (resources/css/components/app-bar.css); this only reads the bar's position and height, once per
* frame while the page scrolls or the bar resizes — its height, because a long title wraps and the
* bar must stick where exactly its row is left showing.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('materialAppBar', () => ({
scrolled: false,
collapsed: false,
height: null,
frame: null,
// Set in init(). Declared here, or Alpine writes them to the outermost x-data scope,
// where a second app bar in the same page scope would take the first one's observer.
schedule: null,
resizes: null,
init() {
this.measure = this.measure.bind(this)
this.schedule = () => {
this.frame ??= requestAnimationFrame(this.measure)
}
this.resizes = new ResizeObserver(this.schedule)
this.resizes.observe(this.$root)
window.addEventListener('scroll', this.schedule, { passive: true })
this.measure()
},
destroy() {
this.resizes.disconnect()
window.removeEventListener('scroll', this.schedule)
cancelAnimationFrame(this.frame)
},
// Bound as the bar's style, so a Livewire morph keeps it.
get measured() {
return this.height === null ? {} : { '--app-bar-measured': `${this.height}px` }
},
measure() {
this.frame = null
const bar = this.$root
this.height = bar.offsetHeight
const style = getComputedStyle(bar)
if (style.position !== 'sticky') {
this.scrolled = this.collapsed = false
return
}
const top = bar.getBoundingClientRect().top
const stuckAt = parseFloat(style.top) || 0
const stuck = window.scrollY > 0 && top <= stuckAt + 0.5
this.collapsed = stuck && stuckAt < 0
this.scrolled = stuck
},
}))
})