Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vuetify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
"lint:fix": "concurrently -n \"tsc,eslint\" \"tsgo -p tsconfig.checks.json --noEmit --pretty\" \"eslint --fix src\""
},
"dependencies": {
"@vuetify/v0": "^1.2.1"
"@vuetify/v0": "^1.2.3"
},
"devDependencies": {
"@date-io/core": "catalog:",
Expand Down
72 changes: 69 additions & 3 deletions packages/vuetify/src/composables/__tests__/display.spec.browser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Composables
import { createDisplay } from '../display'
import { createDisplay, DisplaySymbol, useDisplay } from '../display'

// Utilities
import { page } from '@test'
import { effectScope } from 'vue'
import { page, render } from '@test'
import { defineComponent, effectScope, h, inject, nextTick } from 'vue'

const breakpoints = [
'xs',
Expand Down Expand Up @@ -287,6 +287,72 @@ describe('display', () => {
await expect.poll(() => name.value).toBe('xs')
})

it('should remove the ssr resize listener when the scope stops', async () => {
const add = vi.spyOn(window, 'addEventListener')
const remove = vi.spyOn(window, 'removeEventListener')
const scope = effectScope()

try {
const update = scope.run(() => {
return createDisplay(undefined, { clientWidth: 1024, clientHeight: 768 }).update
})!

update()
await nextTick()

expect(add).toHaveBeenCalledWith('resize', expect.any(Function), { passive: true })

scope.stop()

expect(remove).toHaveBeenCalledWith('resize', expect.any(Function), { passive: true })
} finally {
scope.stop()
add.mockRestore()
remove.mockRestore()
}
})

it('should use the same media query for a component mobileBreakpoint as the global mobile flag', async () => {
await page.viewport(1600, 900)

const original = window.matchMedia.bind(window)
const spy = vi.spyOn(window, 'matchMedia').mockImplementation(query => {
const list = original(query)

if (query !== '(min-width: 1145px)') return list

return new Proxy(list, {
get (target, prop, receiver) {
if (prop === 'matches') return false

const value = Reflect.get(target, prop, receiver)
return typeof value === 'function' ? value.bind(target) : value
},
})
})

try {
let local = false
let global = false

render(defineComponent({
setup () {
const display = inject(DisplaySymbol)!
const { mobile } = useDisplay({ mobile: null, mobileBreakpoint: 'lg' })
local = mobile.value
global = display.mobile.value

return () => h('div')
},
}))

expect(global).toBe(true)
expect(local).toBe(true)
} finally {
spy.mockRestore()
}
})

it('should allow breakpoint strings for mobileBreakpoint', async () => {
const scope = effectScope()
const { mobile } = scope.run(() => createDisplay({ mobileBreakpoint: 'lg' }))!
Expand Down
136 changes: 61 additions & 75 deletions packages/vuetify/src/composables/display.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Utilities
import { computed, inject, onScopeDispose, reactive, shallowRef, toRef, toRefs, watchEffect } from 'vue'
import { getCurrentInstanceName, isNull, isNumber, isObject, mergeDeep, propsFactory } from '@/util'
import { IN_BROWSER, SUPPORTS_TOUCH } from '@/util/globals'
import { computed, effectScope, inject, shallowRef, toRef } from 'vue'
import { createBreakpoints, getCurrentInstanceName, isNull, isNumber, isObject, mergeDeep, propsFactory } from '@/util'
import { IN_BROWSER, SUPPORTS_MATCH_MEDIA, SUPPORTS_TOUCH } from '@/util/globals'

// Types
import type { InjectionKey, PropType, Ref } from 'vue'
Expand Down Expand Up @@ -99,18 +99,6 @@ const parseDisplayOptions = (options: DisplayOptions = defaultDisplayOptions) =>
return mergeDeep(defaultDisplayOptions, options) as InternalDisplayOptions
}

function getClientWidth (ssr?: SSROptions) {
return IN_BROWSER && !ssr
? window.innerWidth
: (isObject(ssr) && ssr.clientWidth) || 0
}

function getClientHeight (ssr?: SSROptions) {
return IN_BROWSER && !ssr
? window.innerHeight
: (isObject(ssr) && ssr.clientHeight) || 0
}

function getPlatform (ssr?: SSROptions): DisplayPlatform {
const userAgent = IN_BROWSER && !ssr
? window.navigator.userAgent
Expand Down Expand Up @@ -152,70 +140,57 @@ function getPlatform (ssr?: SSROptions): DisplayPlatform {
export function createDisplay (options?: DisplayOptions, ssr?: SSROptions): DisplayInstance {
const { thresholds, mobileBreakpoint } = parseDisplayOptions(options)

const height = shallowRef(getClientHeight(ssr))
// `ssr: true` is the boolean form; createBreakpoints only accepts a size.
const ssrSize = isObject(ssr)
? ssr
: ssr
? { clientWidth: 0, clientHeight: 0 }
: undefined

const screen = createBreakpoints({
breakpoints: thresholds,
mobileBreakpoint,
ssr: ssrSize,
})

const platform = shallowRef(getPlatform(ssr))
const state = reactive({} as DisplayInstance)
const width = shallowRef(getClientWidth(ssr))

function updateSize () {
height.value = getClientHeight()
width.value = getClientWidth()
}
// ssr defers the listener until update(), and that call happens after
// createVuetify's scope has returned. The child scope is still stopped
// with it, so unmount removes the listener.
const resizeScope = ssrSize ? effectScope() : undefined

function update () {
updateSize()
if (resizeScope) resizeScope.run(() => screen.update())
else screen.update()
platform.value = getPlatform()
}

// eslint-disable-next-line max-statements
watchEffect(() => {
const xs = width.value < thresholds.sm
const sm = width.value < thresholds.md && !xs
const md = width.value < thresholds.lg && !(sm || xs)
const lg = width.value < thresholds.xl && !(md || sm || xs)
const xl = width.value < thresholds.xxl && !(lg || md || sm || xs)
const xxl = width.value >= thresholds.xxl
const name =
xs ? 'xs'
: sm ? 'sm'
: md ? 'md'
: lg ? 'lg'
: xl ? 'xl'
: 'xxl'
const breakpointValue = isNumber(mobileBreakpoint) ? mobileBreakpoint : thresholds[mobileBreakpoint]
const mobile = width.value < breakpointValue

state.xs = xs
state.sm = sm
state.md = md
state.lg = lg
state.xl = xl
state.xxl = xxl
state.smAndUp = !xs
state.mdAndUp = !(xs || sm)
state.lgAndUp = !(xs || sm || md)
state.xlAndUp = !(xs || sm || md || lg)
state.smAndDown = !(md || lg || xl || xxl)
state.mdAndDown = !(lg || xl || xxl)
state.lgAndDown = !(xl || xxl)
state.xlAndDown = !xxl
state.name = name
state.height = height.value
state.width = width.value
state.mobile = mobile
state.mobileBreakpoint = mobileBreakpoint
state.platform = platform.value
state.thresholds = thresholds
})

if (IN_BROWSER) {
window.addEventListener('resize', updateSize, { passive: true })

onScopeDispose(() => {
window.removeEventListener('resize', updateSize)
}, true)
}

return { ...toRefs(state), update, ssr: !!ssr }
return {
xs: screen.xs,
sm: screen.sm,
md: screen.md,
lg: screen.lg,
xl: screen.xl,
xxl: screen.xxl,
smAndUp: screen.smAndUp,
mdAndUp: screen.mdAndUp,
lgAndUp: screen.lgAndUp,
xlAndUp: screen.xlAndUp,
smAndDown: screen.smAndDown,
mdAndDown: screen.mdAndDown,
lgAndDown: screen.lgAndDown,
xlAndDown: screen.xlAndDown,
name: screen.name,
height: screen.height,
width: screen.width,
mobile: screen.isMobile,
mobileBreakpoint: shallowRef(mobileBreakpoint),
platform,
thresholds: shallowRef(thresholds),
ssr: !!ssr,
update,
} as DisplayInstance
}

export const makeDisplayProps = propsFactory({
Expand All @@ -234,13 +209,24 @@ export function useDisplay (

if (!display) throw new Error('Could not find Vuetify display injection')

function isBelow (px: number, width: number) {
if (SUPPORTS_MATCH_MEDIA) {
return !window.matchMedia(`(min-width: ${px}px)`).matches
Comment thread
J-Sek marked this conversation as resolved.
}

return width < px
Comment thread
J-Sek marked this conversation as resolved.
}

const mobile = computed(() => {
// Reactivity trigger, do not remove. matchMedia() does not subscribe.
const width = display.width.value

if (props.mobile) {
return true
} else if (isNumber(props.mobileBreakpoint)) {
return display.width.value < props.mobileBreakpoint
return isBelow(props.mobileBreakpoint, width)
} else if (props.mobileBreakpoint) {
return display.width.value < display.thresholds.value[props.mobileBreakpoint]
return isBelow(display.thresholds.value[props.mobileBreakpoint], width)
} else if (isNull(props.mobile)) {
return display.mobile.value
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/util/v0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export {

export type { IgnoreAccents } from '@vuetify/v0/utilities'

export { createLocale as createV0Locale, createRtl as createV0Rtl } from '@vuetify/v0/composables'
export { createBreakpoints, createLocale as createV0Locale, createRtl as createV0Rtl } from '@vuetify/v0/composables'

// Kept under core's existing name — `range` shadows local variables in several
// components (VPagination, VRating, VSlider).
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading