Skip to content
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

Make breakpoints use the token feature #39105

Merged
merged 25 commits into from Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)`,
});
azro352 marked this conversation as resolved.
Show resolved Hide resolved

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', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not remove directly which should replace with new test case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not remove existing test cases, add some new test cases for this feature directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yoyo837 @zombieJ

I have no idea how to replace it. What do I do ? Who write the new test ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yoyo837 @zombieJ
Tell me at least, the behaviour you expect to be tested ? And I will try to think about the test implementation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me handle this : )

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