Skip to content

Commit

Permalink
Make breakpoints use the token feature (#39105)
Browse files Browse the repository at this point in the history
* Add XXXL size

* Update index.zh-CN.md

* Update components/list/index.zh-CN.md

Co-authored-by: Amumu <yoyo837@hotmail.com>

* 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 <yoyo837@hotmail.com>
Co-authored-by: 二货机器人 <smith3816@gmail.com>
  • Loading branch information
3 people committed Dec 15, 2022
1 parent 01ae089 commit 18400fb
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 97 deletions.
14 changes: 0 additions & 14 deletions components/_util/__tests__/responsiveObserve.test.ts

This file was deleted.

26 changes: 26 additions & 0 deletions 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(<Demo />);
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();
});
});
137 changes: 74 additions & 63 deletions 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<Breakpoint, string>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;

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<Number, SubscribeFunc>();
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<Number, SubscribeFunc>();
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]);
}
7 changes: 4 additions & 3 deletions components/descriptions/index.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -135,18 +135,19 @@ 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;
}
setScreens(newScreens);
});

return () => {
ResponsiveObserve.unsubscribe(token);
responsiveObserve.unsubscribe(token);
};
}, []);

Expand Down
46 changes: 36 additions & 10 deletions components/grid/__tests__/index.test.tsx
Expand Up @@ -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);
Expand All @@ -13,8 +40,8 @@ describe('Grid', () => {
rtlTest(Row);
rtlTest(Col);

afterEach(() => {
ResponsiveObserve.unregister();
beforeEach(() => {
(global as any).unsubscribeCnt = 0;
});

it('should render Col', () => {
Expand Down Expand Up @@ -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(<Row gutter={{ xs: 20 }} />);
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', () => {
Expand Down
2 changes: 1 addition & 1 deletion 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

Expand Down
7 changes: 4 additions & 3 deletions 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<ScreenMap>({});
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;
Expand Down
8 changes: 5 additions & 3 deletions components/grid/row.tsx
Expand Up @@ -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';

Expand Down Expand Up @@ -103,9 +103,11 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {

const gutterRef = React.useRef<Gutter | [Gutter, Gutter]>(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 (
Expand All @@ -116,7 +118,7 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
setScreens(screen);
}
});
return () => ResponsiveObserve.unsubscribe(token);
return () => responsiveObserve.unsubscribe(token);
}, []);

// ================================== Render ==================================
Expand Down

0 comments on commit 18400fb

Please sign in to comment.