Skip to content

Commit

Permalink
feat: useStableArray, useBreakpoints, useMediaQueries (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan authored Jul 8, 2020
1 parent 5adc7b8 commit 09f95ed
Show file tree
Hide file tree
Showing 18 changed files with 726 additions and 181 deletions.
51 changes: 45 additions & 6 deletions packages/fuselage-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ yarn test

- [useAutoFocus](#useautofocus)
- [Parameters](#parameters)
- [useBreakpoints](#usebreakpoints)
- [useDebouncedCallback](#usedebouncedcallback)
- [Parameters](#parameters-1)
- [useDebouncedReducer](#usedebouncedreducer)
Expand All @@ -41,18 +42,23 @@ yarn test
- [Parameters](#parameters-5)
- [useLazyRef](#uselazyref)
- [Parameters](#parameters-6)
- [useMediaQuery](#usemediaquery)
- [useMediaQueries](#usemediaqueries)
- [Parameters](#parameters-7)
- [useMergedRefs](#usemergedrefs)
- [useMediaQuery](#usemediaquery)
- [Parameters](#parameters-8)
- [useMutableCallback](#usemutablecallback)
- [useMergedRefs](#usemergedrefs)
- [Parameters](#parameters-9)
- [useResizeObserver](#useresizeobserver)
- [useMutableCallback](#usemutablecallback)
- [Parameters](#parameters-10)
- [useSafely](#usesafely)
- [useResizeObserver](#useresizeobserver)
- [Parameters](#parameters-11)
- [useToggle](#usetoggle)
- [useSafely](#usesafely)
- [Parameters](#parameters-12)
- [Comparator](#comparator)
- [useStableArray](#usestablearray)
- [Parameters](#parameters-13)
- [useToggle](#usetoggle)
- [Parameters](#parameters-14)
- [useUniqueId](#useuniqueid)

### useAutoFocus
Expand All @@ -66,6 +72,12 @@ Hook to automatically request focus for an DOM element.

Returns **Ref<{focus: function (options: Options): void}>** the ref which holds the element

### useBreakpoints

Hook to catch which responsive design' breakpoints are active.

Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** an array of the active breakpoint names.

### useDebouncedCallback

Hook to memoize a debounced version of a callback.
Expand Down Expand Up @@ -137,6 +149,16 @@ Hook equivalent to useRef, but with a lazy initialization for computed value.

Returns **any** the ref

### useMediaQueries

Hook to listen to a set of media queries.

#### Parameters

- `queries` **...[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** the CSS3 expressions of media queries

Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** a set of booleans expressing if the media queries match or not

### useMediaQuery

Hook to listen to a media query.
Expand Down Expand Up @@ -192,6 +214,23 @@ which can be safe and asynchronically called even after the component unmounted.

Returns **\[S, D]** a state value and safe dispatcher pair

### Comparator

Type: function (a: T, b: T): [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)

### useStableArray

Hook to create an array with stable identity if its elements are equal.

#### Parameters

- `array` **T** the array
- `compare` **[Comparator](#comparator)** the equality function that checks if two array elements are
equal (optional, default `Object.is`)

Returns **T** the passed array if the elements are NOT equals; the previously
stored array otherwise

### useToggle

Hook to create a toggleable boolean state.
Expand Down
8 changes: 8 additions & 0 deletions packages/fuselage-hooks/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ module.exports = {
testMatch: [
'**/src/**/*.spec.[jt]s?(x)',
],
globals: {
'ts-jest': {
tsConfig: {
noUnusedLocals: false,
noUnusedParameters: false,
},
},
},
};
5 changes: 5 additions & 0 deletions packages/fuselage-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,10 @@
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@rocket.chat/fuselage-tokens": "^0.10.0",
"@types/use-subscription": "^1.0.0",
"use-subscription": "^1.4.1"
}
}
15 changes: 10 additions & 5 deletions packages/fuselage-hooks/src/__mocks__/matchMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import { act } from 'react-dom/test-utils';
export const mediaQueryLists = new Set<MediaQueryList>();

class MediaQueryListMock implements MediaQueryList {
_matches: boolean

_media: string

_onchange: (ev: MediaQueryListEvent) => void | null

changeEventListeners: Set<EventListener>

constructor(media: string) {
this._matches = window.innerWidth <= 968;
this._media = media;
this._onchange = null;
this.changeEventListeners = new Set([
Expand All @@ -23,7 +20,16 @@ class MediaQueryListMock implements MediaQueryList {
}

get matches(): boolean {
return this._matches;
const regex = /^\((min-width|max-width): (\d+)(px|em)\)$/;
if (regex.test(this._media)) {
const [, condition, width, unit] = regex.exec(this._media);
const widthPx = (unit === 'em' && parseInt(width, 10) * 16)
|| (unit === 'px' && parseInt(width, 10));
return (condition === 'min-width' && window.innerWidth >= widthPx)
|| (condition === 'max-width' && window.innerWidth <= widthPx);
}

return false;
}

get media(): string {
Expand Down Expand Up @@ -66,7 +72,6 @@ class MediaQueryListMock implements MediaQueryList {

dispatchEvent(ev: MediaQueryListEvent): boolean {
act(() => {
this._matches = ev.matches;
this._media = ev.media;
this.changeEventListeners.forEach((changeEventListener) => {
changeEventListener(ev);
Expand Down
3 changes: 3 additions & 0 deletions packages/fuselage-hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
export * from './useAutoFocus';
export * from './useBreakpoints';
export * from './useDebouncedCallback';
export * from './useDebouncedReducer';
export * from './useDebouncedState';
export * from './useDebouncedUpdates';
export * from './useDebouncedValue';
export * from './useIsomorphicLayoutEffect';
export * from './useLazyRef';
export * from './useMediaQueries';
export * from './useMediaQuery';
export * from './useMergedRefs';
export * from './useMutableCallback';
export * from './useResizeObserver';
export * from './useSafely';
export * from './useStableArray';
export * from './useToggle';
export * from './useUniqueId';
67 changes: 67 additions & 0 deletions packages/fuselage-hooks/src/useBreakpoints.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import breakpointsDefinitions from '@rocket.chat/fuselage-tokens/breakpoints.json';
import { FunctionComponent, createElement, StrictMode } from 'react';
import { render } from 'react-dom';
import { act } from 'react-dom/test-utils';

import resizeToMock from './__mocks__/resizeTo';
import matchMediaMock from './__mocks__/matchMedia';
import { useBreakpoints } from '.';

beforeAll(() => {
window.resizeTo = resizeToMock;
window.matchMedia = jest.fn(matchMediaMock);
});

beforeEach(() => {
window.resizeTo(1024, 768);
});

it('returns at least the smallest breakpoint name', () => {
let breakpoints: string[];
const TestComponent: FunctionComponent = () => {
breakpoints = useBreakpoints();
return null;
};

act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
document.createElement('div'),
);
});

expect(breakpoints[0]).toBe(breakpointsDefinitions[0].name);
});

it('returns matching breakpoint names', () => {
const initialBreakpoints = breakpointsDefinitions.slice(0, -1);
const finalBreakpoints = breakpointsDefinitions.slice(0, -2);

let breakpoints: string[];
const TestComponent: FunctionComponent = () => {
breakpoints = useBreakpoints();
return null;
};

act(() => {
window.resizeTo(initialBreakpoints[initialBreakpoints.length - 1].minViewportWidth, 768);

render(
createElement(StrictMode, {}, createElement(TestComponent)),
document.createElement('div'),
);
});

expect(breakpoints).toStrictEqual(initialBreakpoints.map((breakpoint) => breakpoint.name));

act(() => {
window.resizeTo(finalBreakpoints[finalBreakpoints.length - 1].minViewportWidth, 768);

render(
createElement(StrictMode, {}, createElement(TestComponent)),
document.createElement('div'),
);
});

expect(breakpoints).toStrictEqual(finalBreakpoints.map((breakpoint) => breakpoint.name));
});
25 changes: 25 additions & 0 deletions packages/fuselage-hooks/src/useBreakpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import breakpointsDefinitions from '@rocket.chat/fuselage-tokens/breakpoints.json';
import { useMemo } from 'react';

import { useMediaQueries } from './useMediaQueries';

const mediaQueries = breakpointsDefinitions
.slice(1)
.map((breakpoint) => `(min-width: ${ breakpoint.minViewportWidth }px)`);

/**
* Hook to catch which responsive design' breakpoints are active.
*
* @returns an array of the active breakpoint names.
*/
export const useBreakpoints = (): string[] => {
const matches = useMediaQueries(...mediaQueries);

return useMemo(() => matches.reduce<string[]>((names, matches, i) => {
if (matches) {
return [...names, breakpointsDefinitions[i + 1].name];
}

return names;
}, [breakpointsDefinitions[0].name]), [matches]);
};
36 changes: 36 additions & 0 deletions packages/fuselage-hooks/src/useMediaQueries.server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @jest-environment node
*/

import { FunctionComponent, createElement, StrictMode } from 'react';
import { renderToString } from 'react-dom/server';

import { useMediaQueries } from '.';

it('returns empty array for undefined media query', () => {
let matches: boolean[];
const TestComponent: FunctionComponent = () => {
matches = useMediaQueries();
return null;
};

renderToString(
createElement(StrictMode, {}, createElement(TestComponent)),
);

expect(matches).toStrictEqual([]);
});

it('returns false for defined media query', () => {
let matches: boolean[];
const TestComponent: FunctionComponent = () => {
matches = useMediaQueries('(max-width: 1024)');
return null;
};

renderToString(
createElement(StrictMode, {}, createElement(TestComponent)),
);

expect(matches).toStrictEqual([false]);
});
Loading

0 comments on commit 09f95ed

Please sign in to comment.