From 18400fbdee8dd14e57fb87962fbac2f618c4b80f Mon Sep 17 00:00:00 2001 From: azro352 <35503478+azro352@users.noreply.github.com> Date: Thu, 15 Dec 2022 07:14:07 +0100 Subject: [PATCH] Make breakpoints use the token feature (#39105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add XXXL size * Update index.zh-CN.md * Update components/list/index.zh-CN.md Co-authored-by: Amumu * Try to use `useToken` for building responsiveMap * Try useResponsiveObserve * Fix useResponsiveObserve * Some try * Some try * Some try * Remove impossible test * Remove impossible test * Fix token.screenXSMax * Try to put test back as token.screenXSMax is fixed * Remove impossible test now * Subscribers, subuid and screen are no longer static * reorganize def * chore: not affect no-related file * chore: not affect no-related file * test: update test case * chore: adjust memo logic Co-authored-by: Amumu Co-authored-by: 二货机器人 --- .../_util/__tests__/responsiveObserve.test.ts | 14 -- .../__tests__/responsiveObserve.test.tsx | 26 ++++ components/_util/responsiveObserve.ts | 137 ++++++++++-------- components/descriptions/index.tsx | 7 +- components/grid/__tests__/index.test.tsx | 46 ++++-- components/grid/demo/responsive.md | 2 +- components/grid/hooks/useBreakpoint.tsx | 7 +- components/grid/row.tsx | 8 +- 8 files changed, 150 insertions(+), 97 deletions(-) delete mode 100644 components/_util/__tests__/responsiveObserve.test.ts create mode 100644 components/_util/__tests__/responsiveObserve.test.tsx diff --git a/components/_util/__tests__/responsiveObserve.test.ts b/components/_util/__tests__/responsiveObserve.test.ts deleted file mode 100644 index 6ab86ec802ea..000000000000 --- a/components/_util/__tests__/responsiveObserve.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import ResponsiveObserve, { responsiveMap } from '../responsiveObserve'; - -describe('Test ResponsiveObserve', () => { - it('test ResponsiveObserve subscribe and unsubscribe', () => { - const { xs } = responsiveMap; - const subscribeFunc = jest.fn(); - const token = ResponsiveObserve.subscribe(subscribeFunc); - expect(ResponsiveObserve.matchHandlers[xs].mql.matches).toBeTruthy(); - expect(subscribeFunc).toHaveBeenCalledTimes(1); - - ResponsiveObserve.unsubscribe(token); - expect(ResponsiveObserve.matchHandlers[xs].mql.removeListener).toHaveBeenCalled(); - }); -}); diff --git a/components/_util/__tests__/responsiveObserve.test.tsx b/components/_util/__tests__/responsiveObserve.test.tsx new file mode 100644 index 000000000000..0bcb6167ca87 --- /dev/null +++ b/components/_util/__tests__/responsiveObserve.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '../../../tests/utils'; +import useResponsiveObserve from '../responsiveObserve'; + +describe('Test ResponsiveObserve', () => { + it('test ResponsiveObserve subscribe and unsubscribe', () => { + let responsiveObserveRef: any; + const Demo = () => { + const responsiveObserve = useResponsiveObserve(); + responsiveObserveRef = responsiveObserve; + return null; + }; + render(); + const subscribeFunc = jest.fn(); + const token = responsiveObserveRef.subscribe(subscribeFunc); + expect( + responsiveObserveRef.matchHandlers[responsiveObserveRef.responsiveMap.xs].mql.matches, + ).toBeTruthy(); + expect(subscribeFunc).toHaveBeenCalledTimes(1); + + responsiveObserveRef.unsubscribe(token); + expect( + responsiveObserveRef.matchHandlers[responsiveObserveRef.responsiveMap.xs].mql.removeListener, + ).toHaveBeenCalled(); + }); +}); diff --git a/components/_util/responsiveObserve.ts b/components/_util/responsiveObserve.ts index 9de911fe62d4..d353b251a6d4 100644 --- a/components/_util/responsiveObserve.ts +++ b/components/_util/responsiveObserve.ts @@ -1,74 +1,85 @@ +import React from 'react'; +import type { GlobalToken } from '../theme/interface'; +import { useToken } from '../theme/internal'; + export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; export type BreakpointMap = Record; export type ScreenMap = Partial>; export type ScreenSizeMap = Partial>; export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']; +type SubscribeFunc = (screens: ScreenMap) => void; -export const responsiveMap: BreakpointMap = { - xs: '(max-width: 575px)', - sm: '(min-width: 576px)', - md: '(min-width: 768px)', - lg: '(min-width: 992px)', - xl: '(min-width: 1200px)', - xxl: '(min-width: 1600px)', -}; +const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({ + xs: `(max-width: ${token.screenXSMax}px)`, + sm: `(min-width: ${token.screenSM}px)`, + md: `(min-width: ${token.screenMD}px)`, + lg: `(min-width: ${token.screenLG}px)`, + xl: `(min-width: ${token.screenXL}px)`, + xxl: `(min-width: ${token.screenXXL}px)`, +}); -type SubscribeFunc = (screens: ScreenMap) => void; -const subscribers = new Map(); -let subUid = -1; -let screens = {}; +export default function useResponsiveObserver() { + const [, token] = useToken(); + const responsiveMap: BreakpointMap = getResponsiveMap(token); -const responsiveObserve = { - matchHandlers: {} as { - [prop: string]: { - mql: MediaQueryList; - listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; - }; - }, - dispatch(pointMap: ScreenMap) { - screens = pointMap; - subscribers.forEach((func) => func(screens)); - return subscribers.size >= 1; - }, - subscribe(func: SubscribeFunc): number { - if (!subscribers.size) this.register(); - subUid += 1; - subscribers.set(subUid, func); - func(screens); - return subUid; - }, - unsubscribe(token: number) { - subscribers.delete(token); - if (!subscribers.size) this.unregister(); - }, - unregister() { - Object.keys(responsiveMap).forEach((screen: Breakpoint) => { - const matchMediaQuery = responsiveMap[screen]; - const handler = this.matchHandlers[matchMediaQuery]; - handler?.mql.removeListener(handler?.listener); - }); - subscribers.clear(); - }, - register() { - Object.keys(responsiveMap).forEach((screen: Breakpoint) => { - const matchMediaQuery = responsiveMap[screen]; - const listener = ({ matches }: { matches: boolean }) => { - this.dispatch({ - ...screens, - [screen]: matches, - }); - }; - const mql = window.matchMedia(matchMediaQuery); - mql.addListener(listener); - this.matchHandlers[matchMediaQuery] = { - mql, - listener, - }; + // To avoid repeat create instance, we add `useMemo` here. + return React.useMemo(() => { + const subscribers = new Map(); + let subUid = -1; + let screens = {}; - listener(mql); - }); - }, -}; + return { + matchHandlers: {} as { + [prop: string]: { + mql: MediaQueryList; + listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; + }; + }, + dispatch(pointMap: ScreenMap) { + screens = pointMap; + subscribers.forEach((func) => func(screens)); + return subscribers.size >= 1; + }, + subscribe(func: SubscribeFunc): number { + if (!subscribers.size) this.register(); + subUid += 1; + subscribers.set(subUid, func); + func(screens); + return subUid; + }, + unsubscribe(paramToken: number) { + subscribers.delete(paramToken); + if (!subscribers.size) this.unregister(); + }, + unregister() { + Object.keys(responsiveMap).forEach((screen: Breakpoint) => { + const matchMediaQuery = responsiveMap[screen]; + const handler = this.matchHandlers[matchMediaQuery]; + handler?.mql.removeListener(handler?.listener); + }); + subscribers.clear(); + }, + register() { + Object.keys(responsiveMap).forEach((screen: Breakpoint) => { + const matchMediaQuery = responsiveMap[screen]; + const listener = ({ matches }: { matches: boolean }) => { + this.dispatch({ + ...screens, + [screen]: matches, + }); + }; + const mql = window.matchMedia(matchMediaQuery); + mql.addListener(listener); + this.matchHandlers[matchMediaQuery] = { + mql, + listener, + }; -export default responsiveObserve; + listener(mql); + }); + }, + responsiveMap, + }; + }, [token]); +} diff --git a/components/descriptions/index.tsx b/components/descriptions/index.tsx index 92f90fa1d44f..b3eea24065d5 100644 --- a/components/descriptions/index.tsx +++ b/components/descriptions/index.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { ConfigContext } from '../config-provider'; import { cloneElement } from '../_util/reactNode'; import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve'; -import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve'; +import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve'; import warning from '../_util/warning'; import DescriptionsItem from './Item'; import Row from './Row'; @@ -135,10 +135,11 @@ function Descriptions({ const mergedColumn = getColumn(column, screens); const [wrapSSR, hashId] = useStyle(prefixCls); + const responsiveObserve = useResponsiveObserve(); // Responsive React.useEffect(() => { - const token = ResponsiveObserve.subscribe((newScreens) => { + const token = responsiveObserve.subscribe((newScreens) => { if (typeof column !== 'object') { return; } @@ -146,7 +147,7 @@ function Descriptions({ }); return () => { - ResponsiveObserve.unsubscribe(token); + responsiveObserve.unsubscribe(token); }; }, []); diff --git a/components/grid/__tests__/index.test.tsx b/components/grid/__tests__/index.test.tsx index 3bbe53e44f47..5f35715888cc 100644 --- a/components/grid/__tests__/index.test.tsx +++ b/components/grid/__tests__/index.test.tsx @@ -2,9 +2,36 @@ import React from 'react'; import { Col, Row } from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import ResponsiveObserve from '../../_util/responsiveObserve'; import useBreakpoint from '../hooks/useBreakpoint'; -import { render, act } from '../../../tests/utils'; +import { render } from '../../../tests/utils'; + +// Mock for `responsiveObserve` to test `unsubscribe` call +jest.mock('../../_util/responsiveObserve', () => { + const modules = jest.requireActual('../../_util/responsiveObserve'); + const originHook = modules.default; + + const useMockResponsiveObserver = (...args: any[]) => { + const entity = originHook(...args); + if (!entity.unsubscribe.mocked) { + const originUnsubscribe = entity.unsubscribe; + entity.unsubscribe = (...uArgs: any[]) => { + const inst = global as any; + inst.unsubscribeCnt = (inst.unsubscribeCnt || 0) + 1; + + originUnsubscribe.call(entity, ...uArgs); + }; + entity.unsubscribe.mocked = true; + } + + return entity; + }; + + return { + ...modules, + __esModule: true, + default: useMockResponsiveObserver, + }; +}); describe('Grid', () => { mountTest(Row); @@ -13,8 +40,8 @@ describe('Grid', () => { rtlTest(Row); rtlTest(Col); - afterEach(() => { - ResponsiveObserve.unregister(); + beforeEach(() => { + (global as any).unsubscribeCnt = 0; }); it('should render Col', () => { @@ -88,13 +115,12 @@ describe('Grid', () => { expect(asFragment().firstChild).toMatchSnapshot(); }); - it('ResponsiveObserve.unsubscribe should be called when unmounted', () => { - const Unmount = jest.spyOn(ResponsiveObserve, 'unsubscribe'); + it('useResponsiveObserve.unsubscribe should be called when unmounted', () => { const { unmount } = render(); - act(() => { - unmount(); - }); - expect(Unmount).toHaveBeenCalled(); + const called: number = (global as any).unsubscribeCnt; + + unmount(); + expect((global as any).unsubscribeCnt).toEqual(called + 1); }); it('should work correct when gutter is object', () => { diff --git a/components/grid/demo/responsive.md b/components/grid/demo/responsive.md index 76646cabdb8c..c284a628b165 100644 --- a/components/grid/demo/responsive.md +++ b/components/grid/demo/responsive.md @@ -1,6 +1,6 @@ ## zh-CN -参照 Bootstrap 的 [响应式设计](http://getbootstrap.com/css/#grid-media-queries),预设六个响应尺寸:`xs` `sm` `md` `lg` `xl` `xxl`。 +参照 Bootstrap 的 [响应式设计](http://getbootstrap.com/css/#grid-media-queries),预设六个响应尺寸:`xs` `sm` `md` `lg` `xl` `xxl`。 ## en-US diff --git a/components/grid/hooks/useBreakpoint.tsx b/components/grid/hooks/useBreakpoint.tsx index 0435b198b81b..a66e2bd8908d 100644 --- a/components/grid/hooks/useBreakpoint.tsx +++ b/components/grid/hooks/useBreakpoint.tsx @@ -1,21 +1,22 @@ import { useEffect, useRef } from 'react'; import useForceUpdate from '../../_util/hooks/useForceUpdate'; import type { ScreenMap } from '../../_util/responsiveObserve'; -import ResponsiveObserve from '../../_util/responsiveObserve'; +import useResponsiveObserve from '../../_util/responsiveObserve'; function useBreakpoint(refreshOnChange: boolean = true): ScreenMap { const screensRef = useRef({}); const forceUpdate = useForceUpdate(); + const responsiveObserve = useResponsiveObserve(); useEffect(() => { - const token = ResponsiveObserve.subscribe((supportScreens) => { + const token = responsiveObserve.subscribe((supportScreens) => { screensRef.current = supportScreens; if (refreshOnChange) { forceUpdate(); } }); - return () => ResponsiveObserve.unsubscribe(token); + return () => responsiveObserve.unsubscribe(token); }, []); return screensRef.current; diff --git a/components/grid/row.tsx b/components/grid/row.tsx index 96bfc87f4c07..cf198f6a41f7 100644 --- a/components/grid/row.tsx +++ b/components/grid/row.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { ConfigContext } from '../config-provider'; import useFlexGapSupport from '../_util/hooks/useFlexGapSupport'; import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve'; -import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve'; +import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve'; import RowContext from './RowContext'; import { useRowStyle } from './style'; @@ -103,9 +103,11 @@ const Row = React.forwardRef((props, ref) => { const gutterRef = React.useRef(gutter); + const responsiveObserve = useResponsiveObserve(); + // ================================== Effect ================================== React.useEffect(() => { - const token = ResponsiveObserve.subscribe((screen) => { + const token = responsiveObserve.subscribe((screen) => { setCurScreens(screen); const currentGutter = gutterRef.current || 0; if ( @@ -116,7 +118,7 @@ const Row = React.forwardRef((props, ref) => { setScreens(screen); } }); - return () => ResponsiveObserve.unsubscribe(token); + return () => responsiveObserve.unsubscribe(token); }, []); // ================================== Render ==================================