ytptube/ui/app/composables/useMediaQuery.ts
2025-08-19 19:27:45 +03:00

61 lines
1.5 KiB
TypeScript

import { ref, onMounted, onBeforeUnmount, shallowReadonly, type Ref } from 'vue'
export interface MediaQueryOptions {
/**
* A custom media query string.
* Example: "(max-width: 640px) or (hover: none)"
* If provided, takes precedence over maxWidth.
*/
query?: string
/**
* Max width in px considered true.
* Ignored if `query` is provided. Default: 768.
*/
maxWidth?: number
}
/**
* Reactive state of a CSS media query.
*/
export function useMediaQuery(options: MediaQueryOptions = {}): Readonly<Ref<boolean>> {
const query = options.query ?? `(max-width: ${options.maxWidth ?? 1024}px)`
const matches = ref(false)
let mql: MediaQueryList | null = null
let onChange: ((ev: MediaQueryListEvent) => void) | null = null
const setup = () => {
if ('undefined' === typeof window || !window.matchMedia) {
return
}
mql = window.matchMedia(query)
matches.value = mql.matches
onChange = ev => { matches.value = ev.matches }
if ('addEventListener' in mql) {
mql.addEventListener('change', onChange as EventListener)
return
}
// @ts-expect-error legacy Safari
mql.addListener(onChange)
}
const teardown = () => {
if (!mql || !onChange) return
if ('removeEventListener' in mql) {
mql.removeEventListener('change', onChange as EventListener)
} else {
// @ts-expect-error legacy Safari
mql.removeListener(onChange)
}
mql = null
onChange = null
}
onMounted(setup)
onBeforeUnmount(teardown)
return shallowReadonly(matches)
}