ytptube/ui/app/composables/useMediaQuery.ts
2026-02-20 22:46:30 +03:00

63 lines
1.6 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);
}