tests / browser (chrome, chromium) (push) Successful in 3m34s
tests / browser (safari, webkit) (push) Successful in 6m2s
tests / lint (push) Successful in 59s
tests / feature (8.4) (push) Successful in 1m6s
tests / feature (8.5) (push) Successful in 1m9s
tests / browser (firefox, firefox) (push) Successful in 4m20s
M3 Expressive top app bars (small, centered, medium and large flexible, search) that collapse with CSS sticky, docked and floating toolbars, primary and secondary tabs with a view-transition indicator, section navigation, the account menu and theme toggles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
57 lines
2.0 KiB
JavaScript
57 lines
2.0 KiB
JavaScript
/**
|
|
* `materialTabs`: the behaviour of `<x-tabs>` — WAI-ARIA's tabs with automatic activation.
|
|
*
|
|
* The arrow keys move along the tablist (mirrored on a right-to-left page), Home and End jump to
|
|
* its ends, disabled tabs are passed over, focus follows the choice, and only the chosen tab is in
|
|
* the Tab order. A change of tab runs in a view transition, which moves the active indicator from
|
|
* the old tab to the new one and cross-fades the panels — unless the visitor prefers reduced motion
|
|
* or the browser has no view transitions, when it simply changes.
|
|
*/
|
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
window.Alpine.data('materialTabs', () => ({
|
|
tabs() {
|
|
return [...this.$refs.bar.querySelectorAll('[role="tab"]:not(:disabled)')]
|
|
},
|
|
|
|
choose(name, focus = false) {
|
|
const apply = () => {
|
|
this.selected = name
|
|
|
|
return this.$nextTick()
|
|
}
|
|
|
|
if (this.selected === name || reducedMotion.matches || !document.startViewTransition) {
|
|
apply()
|
|
} else {
|
|
document.startViewTransition(apply)
|
|
}
|
|
|
|
if (focus) {
|
|
this.$refs.bar.querySelector(`[data-tab="${CSS.escape(name)}"]`)?.focus()
|
|
}
|
|
},
|
|
|
|
step(by) {
|
|
const tabs = this.tabs()
|
|
const rtl = getComputedStyle(this.$refs.bar).direction === 'rtl'
|
|
const at = tabs.findIndex((tab) => tab.dataset.tab === this.selected)
|
|
const next = tabs[(at + (rtl ? -by : by) + tabs.length) % tabs.length]
|
|
|
|
if (next) {
|
|
this.choose(next.dataset.tab, true)
|
|
}
|
|
},
|
|
|
|
edge(last) {
|
|
const tabs = this.tabs()
|
|
const tab = last ? tabs.at(-1) : tabs[0]
|
|
|
|
if (tab) {
|
|
this.choose(tab.dataset.tab, true)
|
|
}
|
|
},
|
|
}))
|
|
})
|