Skip to content

fix(ssr)!: include device pixel ratio in w/h calculation #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 21, 2024
Merged
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: 2 additions & 0 deletions src/runtime/plugins/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export interface ClientHintRequestFeatures {
prefersReducedMotionAvailable: boolean
viewportHeightAvailable: boolean
viewportWidthAvailable: boolean
devicePixelRatioAvailable: boolean
}
export interface SSRClientHints extends ClientHintRequestFeatures {
prefersColorScheme?: 'dark' | 'light' | 'no-preference'
prefersReducedMotion?: 'no-preference' | 'reduce'
viewportHeight?: number
viewportWidth?: number
devicePixelRatio?: number
colorSchemeFromCookie?: string
colorSchemeCookie?: string
}
Expand Down
95 changes: 48 additions & 47 deletions src/runtime/plugins/vuetify-client-hints.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,57 +60,58 @@ const plugin: Plugin<{
window.location.reload()
}

if (viewportSize || (prefersColorScheme && prefersColorSchemeOptions)) {
// restore SSR state
nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => {
// on client, we update the display to avoid hydration mismatch on page refresh
// there will be some hydration mismatch since the headers sent by the user agent may not be accurate
if (viewportSize) {
const clientWidth = state.value.viewportWidth
const clientHeight = state.value.viewportHeight
vuetifyOptions.ssr = typeof clientWidth === 'number'
? {
clientWidth,
clientHeight,
}
: true
}

// update the theme
if (prefersColorScheme && prefersColorSchemeOptions) {
if (vuetifyOptions.theme === false) {
vuetifyOptions.theme = { defaultTheme: state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme }
}
else {
vuetifyOptions.theme = vuetifyOptions.theme ?? {}
vuetifyOptions.theme.defaultTheme = state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme
}
}
})
// restore SSR state
nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => {
// on client, we update the display to avoid hydration mismatch on page refresh
// there will be some hydration mismatch since the headers sent by the user agent may not be accurate
if (viewportSize) {
const clientWidth = state.value.viewportWidth
const clientHeight = state.value.viewportHeight
vuetifyOptions.ssr = typeof clientWidth === 'number'
? {
clientWidth,
clientHeight,
}
: true
}
else {
vuetifyOptions.ssr = true
}

// update theme logic
// update the theme
if (prefersColorScheme && prefersColorSchemeOptions) {
const themeCookie = state.value.colorSchemeCookie
if (themeCookie) {
nuxtApp.hook('app:beforeMount', () => {
const vuetify = useNuxtApp().$vuetify
// update the theme
const cookieName = prefersColorSchemeOptions.cookieName
const parseCookieName = `${cookieName}=`
const cookieEntry = `${parseCookieName}${state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};`
watch(vuetify.theme.global.name, (newThemeName) => {
document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`)
})
if (prefersColorSchemeOptions.useBrowserThemeOnly) {
const { darkThemeName, lightThemeName } = prefersColorSchemeOptions
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
prefersDark.addEventListener('change', (e) => {
vuetify.theme.global.name.value = e.matches ? darkThemeName : lightThemeName
})
}
})
if (vuetifyOptions.theme === false) {
vuetifyOptions.theme = { defaultTheme: state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme }
}
else {
vuetifyOptions.theme = vuetifyOptions.theme ?? {}
vuetifyOptions.theme.defaultTheme = state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme
}
}
})

// update theme logic
if (prefersColorScheme && prefersColorSchemeOptions) {
const themeCookie = state.value.colorSchemeCookie
if (themeCookie) {
nuxtApp.hook('app:beforeMount', () => {
const vuetify = useNuxtApp().$vuetify
// update the theme
const cookieName = prefersColorSchemeOptions.cookieName
const parseCookieName = `${cookieName}=`
const cookieEntry = `${parseCookieName}${state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};`
watch(vuetify.theme.global.name, (newThemeName) => {
document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`)
})
if (prefersColorSchemeOptions.useBrowserThemeOnly) {
const { darkThemeName, lightThemeName } = prefersColorSchemeOptions
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
prefersDark.addEventListener('change', (e) => {
vuetify.theme.global.name.value = e.matches ? darkThemeName : lightThemeName
})
}
})
}
}

return {
Expand Down
34 changes: 32 additions & 2 deletions src/runtime/plugins/vuetify-client-hints.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const AcceptClientHintsHeaders = {
prefersReducedMotion: 'Sec-CH-Prefers-Reduced-Motion',
viewportHeight: 'Sec-CH-Viewport-Height',
viewportWidth: 'Sec-CH-Viewport-Width',
devicePixelRatio: 'Sec-CH-DPR',
}

type AcceptClientHintsHeadersKey = keyof typeof AcceptClientHintsHeaders
Expand All @@ -38,7 +39,7 @@ const plugin: Plugin<{
ssrClientHints: UnwrapNestedRefs<SSRClientHints>
}> = defineNuxtPlugin({
name: 'vuetify:client-hints:server:plugin',
order: -25,
order: 24,
parallel: true,
setup(nuxtApp) {
const state = useState<SSRClientHints>(VuetifyHTTPClientHints)
Expand Down Expand Up @@ -107,23 +108,29 @@ type BrowserFeatures = Record<AcceptClientHintsHeadersKey, BrowserFeatureAvailab
// Tests for Browser compatibility
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion#browser_compatibility
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme#browser_compatibility
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DPR#browser_compatibility
const chromiumBasedBrowserFeatures: BrowserFeatures = {
prefersColorScheme: (_, v) => v[0] >= 93,
prefersReducedMotion: (_, v) => v[0] >= 108,
viewportHeight: (_, v) => v[0] >= 108,
viewportWidth: (_, v) => v[0] >= 108,
devicePixelRatio: (_, v) => v[0] >= 46,
}
const allowedBrowsers: [browser: Browser, features: BrowserFeatures][] = [
// 'edge',
// 'edge-ios',
['chrome', chromiumBasedBrowserFeatures],
['edge-chromium', chromiumBasedBrowserFeatures],
['edge-chromium', {
...chromiumBasedBrowserFeatures,
devicePixelRatio: (_, v) => v[0] >= 79,
}],
['chromium-webview', chromiumBasedBrowserFeatures],
['opera', {
prefersColorScheme: (android, v) => v[0] >= (android ? 66 : 79),
prefersReducedMotion: (android, v) => v[0] >= (android ? 73 : 94),
viewportHeight: (android, v) => v[0] >= (android ? 73 : 94),
viewportWidth: (android, v) => v[0] >= (android ? 73 : 94),
devicePixelRatio: (_, v) => v[0] >= 33,
}],
]

Expand Down Expand Up @@ -164,6 +171,7 @@ function lookupClientHints(
prefersReducedMotionAvailable: false,
viewportHeightAvailable: false,
viewportWidthAvailable: false,
devicePixelRatioAvailable: false,
}

if (userAgent == null || userAgent.type !== 'browser')
Expand All @@ -178,6 +186,7 @@ function lookupClientHints(
if (ssrClientHintsConfiguration.viewportSize) {
features.viewportHeightAvailable = browserFeatureAvailable(userAgent, 'viewportHeight')
features.viewportWidthAvailable = browserFeatureAvailable(userAgent, 'viewportWidth')
features.devicePixelRatioAvailable = browserFeatureAvailable(userAgent, 'devicePixelRatio')
}

return features
Expand Down Expand Up @@ -266,6 +275,25 @@ function collectClientHints(
hints.viewportWidth = ssrClientHintsConfiguration.clientWidth
}

if (hints.devicePixelRatioAvailable && ssrClientHintsConfiguration.viewportSize) {
const header = headers[AcceptClientHintsRequestHeaders.devicePixelRatio]
if (header) {
hints.firstRequest = false
try {
hints.devicePixelRatio = Number.parseFloat(header)
if (!Number.isNaN(hints.devicePixelRatio) && hints.devicePixelRatio > 0) {
if (typeof hints.viewportWidth === 'number')
hints.viewportWidth = Math.round(hints.viewportWidth / hints.devicePixelRatio)
if (typeof hints.viewportHeight === 'number')
hints.viewportHeight = Math.round(hints.viewportHeight / hints.devicePixelRatio)
}
}
catch {
// just ignore
}
}
}

return hints
}

Expand All @@ -292,6 +320,8 @@ function writeClientHintsResponseHeaders(
if (ssrClientHintsConfiguration.viewportSize && clientHintsRequest.viewportHeightAvailable && clientHintsRequest.viewportWidthAvailable) {
writeClientHintHeaders(AcceptClientHintsHeaders.viewportHeight, headers)
writeClientHintHeaders(AcceptClientHintsHeaders.viewportWidth, headers)
if (clientHintsRequest.devicePixelRatioAvailable)
writeClientHintHeaders(AcceptClientHintsHeaders.devicePixelRatio, headers)
}

if (Object.keys(headers).length === 0)
Expand Down