Files
livewire-material/resources/js/fab.js
T
Andreas Reinhold / reiniandClaude Opus 5 c065d940af Draw the FAB menu without Tailwind
<x-fab-menu> renders data-md-fab-menu with its trigger
data-md-fab-menu-trigger, data-md-color and data-md-position; a
close icon swaps in on aria-expanded instead of Tailwind's group
variant. fab-menu.css draws FabMenuBaselineTokens' 56px trigger,
its round-to-full morph on the default springs (ACT-19), the 8px
list padding and its calc(100dvh-128px) scroll cap behind the fixed
close button (ACT-32), and the [data-md-fab-menu-popover]:not(:popover-open)
rule that drives fab-menu-item's exit (ACT-33) — actions.css's last
two rules, moved here, empty it, so it and its tailwind.css import
are deleted; fab.js's stale reference to it is corrected to fab.css
(plan step 36, actions — the group is now fully off Tailwind).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 16:20:45 +02:00

75 lines
2.5 KiB
JavaScript

/**
* `materialFab`: `<x-fab collapse-on-scroll>` — M3's extended FAB that collapses to a FAB while the
* page scrolls down and extends again on the way back up, or once the page is at the top.
*
* Only the flag lives here. The morph itself is CSS (resources/css/components/fab.css), which is
* how it stays on the spatial spring and how reduced motion makes it instant without a second
* path through this file.
*
* It watches the window, which is what a FAB pinned to the corner of the page scrolls against. A
* FAB inside a scrolling pane of its own is not this.
*/
// Scrolling is noisy — a wheel's own wobble, a rubber-band bounce at either end — and a FAB that
// flipped on every pixel would never be still. A move has to be worth this much to count as a
// direction, and one worth less is kept and added to the next.
const STEP_PX = 8
// Within this much of the top the FAB is extended whichever way the page was last going: M3
// re-expands it "at the bottom of the view", which on a web page is where the page begins.
const TOP_PX = 24
document.addEventListener('alpine:init', () => {
window.Alpine.data('materialFab', () => ({
collapsed: false,
lastY: 0,
ticking: false,
listeners: [],
init() {
this.lastY = Math.max(window.scrollY, 0)
this.listen(window, 'scroll', () => this.queue(), { passive: true })
},
/** One reading a frame: `scroll` fires far more often than anything can be drawn. */
queue() {
if (this.ticking) {
return
}
this.ticking = true
requestAnimationFrame(() => {
this.ticking = false
this.measure()
})
},
measure() {
const y = Math.max(window.scrollY, 0)
const moved = y - this.lastY
if (y <= TOP_PX) {
this.collapsed = false
} else if (moved > STEP_PX) {
this.collapsed = true
} else if (moved < -STEP_PX) {
this.collapsed = false
}
if (Math.abs(moved) > STEP_PX || y <= TOP_PX) {
this.lastY = y
}
},
listen(target, type, handler, options) {
target.addEventListener(type, handler, options)
this.listeners.push(() => target.removeEventListener(type, handler, options))
},
destroy() {
this.listeners.forEach((remove) => remove())
},
}))
})