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 65d535699eab..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 ae04c14f6681..c7243e911231 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';
@@ -65,7 +65,7 @@ function getFilledItem(
}
function getRows(children: React.ReactNode, column: number) {
- const childNodes = toArray(children).filter(n => n);
+ const childNodes = toArray(children).filter((n) => n);
const rows: React.ReactElement[][] = [];
let tmpRow: React.ReactElement[] = [];
@@ -132,9 +132,12 @@ function Descriptions({
const [screens, setScreens] = React.useState({});
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;
}
@@ -142,7 +145,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 47a19a5f114a..e882e0daab19 100644
--- a/components/grid/__tests__/index.test.tsx
+++ b/components/grid/__tests__/index.test.tsx
@@ -2,10 +2,37 @@ import React, { useState } 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, fireEvent } 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);
mountTest(Col);
@@ -13,8 +40,8 @@ describe('Grid', () => {
rtlTest(Row);
rtlTest(Col);
- afterEach(() => {
- ResponsiveObserve.unregister();
+ beforeEach(() => {
+ (global as any).unsubscribeCnt = 0;
});
it('should render Col', () => {
@@ -48,14 +75,14 @@ describe('Grid', () => {
it('when typeof gutter is object array in large screen', () => {
jest.spyOn(window, 'matchMedia').mockImplementation(
- query =>
+ (query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(min-width: 1200px)' });
},
removeListener: jest.fn(),
matches: query === '(min-width: 1200px)',
- } as any),
+ }) as any,
);
const { container, asFragment } = render(
@@ -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', () => {
@@ -116,14 +142,14 @@ describe('Grid', () => {
it('should work with useBreakpoint', () => {
const matchMediaSpy = jest.spyOn(window, 'matchMedia');
matchMediaSpy.mockImplementation(
- query =>
+ (query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
- } as any),
+ }) as any,
);
let screensVar;
@@ -147,14 +173,14 @@ describe('Grid', () => {
it('should align by responsive align prop', () => {
const matchMediaSpy = jest.spyOn(window, 'matchMedia');
matchMediaSpy.mockImplementation(
- query =>
+ (query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
- } as any),
+ }) as any,
);
const { container } = render(
);
expect(container.innerHTML).toContain('ant-row-middle');
@@ -167,14 +193,14 @@ describe('Grid', () => {
it('should justify by responsive justify prop', () => {
const matchMediaSpy = jest.spyOn(window, 'matchMedia');
matchMediaSpy.mockImplementation(
- query =>
+ (query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
- } as any),
+ }) as any,
);
const { container } = render(
);
expect(container.innerHTML).toContain('ant-row-center');
diff --git a/components/grid/demo/responsive.md b/components/grid/demo/responsive.md
index c6d6808680e3..3a9c629f71a6 100644
--- a/components/grid/demo/responsive.md
+++ b/components/grid/demo/responsive.md
@@ -7,7 +7,7 @@ title:
## 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 4b8780c684b2..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 1770dbe460cb..2ec2417c03c9 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 { tuple } from '../_util/type';
import RowContext from './RowContext';
@@ -99,9 +99,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 (
@@ -112,7 +114,7 @@ const Row = React.forwardRef((props, ref) => {
setScreens(screen);
}
});
- return () => ResponsiveObserve.unsubscribe(token);
+ return () => responsiveObserve.unsubscribe(token);
}, []);
// ================================== Render ==================================