fix(ui): stabilize mobile hydration and layout overflow
This commit is contained in:
parent
b9476733fc
commit
df05999c0f
@ -185,7 +185,7 @@ export function PlatformSplitLayout({
|
||||
>
|
||||
{leftContent}
|
||||
</aside>
|
||||
<div className="min-w-0 flex-1 overflow-y-auto rounded-xl p-3 max-lg:p-2"
|
||||
<div className="min-w-0 flex-1 overflow-x-hidden overflow-y-auto rounded-xl p-3 max-lg:p-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-surface-container-lowest)',
|
||||
boxShadow: '0px 4px 20px rgba(45,51,53,0.06)',
|
||||
@ -195,7 +195,7 @@ export function PlatformSplitLayout({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-y-auto rounded-xl p-3 max-lg:p-2"
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overflow-y-auto rounded-xl p-3 max-lg:p-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-surface-container-lowest)',
|
||||
boxShadow: '0px 4px 20px rgba(45,51,53,0.06)',
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { PlatformSplitLayout } from '../PlatformSplitLayout'
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: jest.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('PlatformSplitLayout', () => {
|
||||
it('默认使用统一的右栏宽度变量', () => {
|
||||
const { container } = render(
|
||||
@ -16,6 +22,20 @@ describe('PlatformSplitLayout', () => {
|
||||
expect(rightPanel).toHaveClass('w-[var(--layout-right-panel-width)]')
|
||||
expect(rightPanel).toHaveClass('max-w-[var(--layout-right-panel-max-width)]')
|
||||
})
|
||||
|
||||
it('收住页面级横向溢出', () => {
|
||||
const { container } = render(
|
||||
<PlatformSplitLayout
|
||||
leftContent={<div className="w-[200vw]">wide content</div>}
|
||||
rightContent={null}
|
||||
/>,
|
||||
)
|
||||
|
||||
const contentPanel = container.querySelector('section > div')
|
||||
|
||||
expect(contentPanel).toHaveClass('overflow-x-hidden')
|
||||
})
|
||||
|
||||
it('允许页面级覆盖右栏宽度', () => {
|
||||
const { container } = render(
|
||||
<PlatformSplitLayout
|
||||
|
||||
60
furniture-3d-system/src/hooks/useMediaQuery.test.tsx
Normal file
60
furniture-3d-system/src/hooks/useMediaQuery.test.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { useMediaQuery } from './useMediaQuery'
|
||||
|
||||
function MediaQueryProbe({ query = '(max-width: 767px)' }: { query?: string }) {
|
||||
const matches = useMediaQuery(query)
|
||||
|
||||
return <output data-testid="media-query">{String(matches)}</output>
|
||||
}
|
||||
|
||||
function installMatchMedia(initialMatches: boolean) {
|
||||
let matches = initialMatches
|
||||
const listeners = new Set<(event: MediaQueryListEvent) => void>()
|
||||
|
||||
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
|
||||
if (event === 'change') listeners.add(listener)
|
||||
}),
|
||||
removeEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
|
||||
if (event === 'change') listeners.delete(listener)
|
||||
}),
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}))
|
||||
|
||||
return {
|
||||
setMatches(nextMatches: boolean) {
|
||||
matches = nextMatches
|
||||
act(() => {
|
||||
listeners.forEach((listener) =>
|
||||
listener({ matches: nextMatches, media: '(max-width: 767px)' } as MediaQueryListEvent),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('useMediaQuery', () => {
|
||||
it('uses a stable server snapshot for hydration', () => {
|
||||
installMatchMedia(true)
|
||||
|
||||
expect(renderToString(<MediaQueryProbe />)).toContain('false')
|
||||
})
|
||||
|
||||
it('subscribes to client media query changes', () => {
|
||||
const media = installMatchMedia(false)
|
||||
|
||||
render(<MediaQueryProbe />)
|
||||
|
||||
expect(screen.getByTestId('media-query')).toHaveTextContent('false')
|
||||
|
||||
media.setMatches(true)
|
||||
|
||||
expect(screen.getByTestId('media-query')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
/**
|
||||
* 媒体查询 Hook
|
||||
@ -13,30 +13,32 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
* const isDesktop = useMediaQuery('(min-width: 1024px)')
|
||||
*/
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const getMatches = useCallback((): boolean => {
|
||||
const subscribe = useCallback(
|
||||
(onStoreChange: () => void) => {
|
||||
if (typeof window === 'undefined') return () => {}
|
||||
|
||||
const mql = window.matchMedia(query)
|
||||
mql.addEventListener('change', onStoreChange)
|
||||
|
||||
return () => {
|
||||
mql.removeEventListener('change', onStoreChange)
|
||||
}
|
||||
},
|
||||
[query],
|
||||
)
|
||||
|
||||
const getSnapshot = useCallback(() => {
|
||||
if (typeof window === 'undefined') return false
|
||||
return window.matchMedia(query).matches
|
||||
}, [query])
|
||||
|
||||
const [matches, setMatches] = useState<boolean>(getMatches)
|
||||
const getServerSnapshot = useCallback(() => false, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const mql = window.matchMedia(query)
|
||||
|
||||
// 立即同步一次
|
||||
setMatches(mql.matches)
|
||||
|
||||
function handleChange(event: MediaQueryListEvent) {
|
||||
setMatches(event.matches)
|
||||
}
|
||||
|
||||
mql.addEventListener('change', handleChange)
|
||||
return () => mql.removeEventListener('change', handleChange)
|
||||
}, [query])
|
||||
|
||||
return matches
|
||||
return useSyncExternalStore(
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getServerSnapshot,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user