Skip to content

Commit

Permalink
Backport xxxl-grid-size from ant-design#39105
Browse files Browse the repository at this point in the history
  • Loading branch information
Khez committed Apr 17, 2024
1 parent c6116fa commit 856664d
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 105 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';

Check failure on line 2 in components/_util/responsiveObserve.ts

View workflow job for this annotation

GitHub Actions / lint

Cannot find module '../theme/interface' or its corresponding type declarations.

Check failure on line 2 in components/_util/responsiveObserve.ts

View workflow job for this annotation

GitHub Actions / tsx-demo

Cannot find module '../theme/interface' or its corresponding type declarations.
import { useToken } from '../theme/internal';

Check failure on line 3 in components/_util/responsiveObserve.ts

View workflow job for this annotation

GitHub Actions / lint

Cannot find module '../theme/internal' or its corresponding type declarations.

Check failure on line 3 in components/_util/responsiveObserve.ts

View workflow job for this annotation

GitHub Actions / tsx-demo

Cannot find module '../theme/internal' or its corresponding type declarations.

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]);
}
11 changes: 7 additions & 4 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 @@ -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[] = [];
Expand Down Expand Up @@ -132,17 +132,20 @@ function Descriptions({
const [screens, setScreens] = React.useState<ScreenMap>({});
const mergedColumn = getColumn(column, screens);

const [wrapSSR, hashId] = useStyle(prefixCls);

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / lint

'wrapSSR' is declared but its value is never read.

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / lint

'hashId' is declared but its value is never read.

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / lint

Cannot find name 'useStyle'.

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / tsx-demo

'wrapSSR' is declared but its value is never read.

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / tsx-demo

'hashId' is declared but its value is never read.

Check failure on line 135 in components/descriptions/index.tsx

View workflow job for this annotation

GitHub Actions / tsx-demo

Cannot find name 'useStyle'.
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
60 changes: 43 additions & 17 deletions components/grid/__tests__/index.test.tsx
Expand Up @@ -2,19 +2,46 @@ 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);

rtlTest(Row);
rtlTest(Col);

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

it('should render Col', () => {
Expand Down Expand Up @@ -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(
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 All @@ -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;
Expand All @@ -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(<Row align="middle" />);
expect(container.innerHTML).toContain('ant-row-middle');
Expand All @@ -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(<Row justify="center" />);
expect(container.innerHTML).toContain('ant-row-center');
Expand Down
2 changes: 1 addition & 1 deletion components/grid/demo/responsive.md
Expand Up @@ -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

Expand Down

0 comments on commit 856664d

Please sign in to comment.