Skip to content

Commit

Permalink
feat: implement new hook - useScreenOrientation
Browse files Browse the repository at this point in the history
Checks if screen is in `portrait` or `landscape` orientation
and automatically re-renders on orientation change.

#
  • Loading branch information
xobotyi authored and JoeDuncko committed Feb 25, 2022
1 parent b18cc28 commit 107cc21
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ Coming from `react-use`? Check out our
— Tracks the state of CSS media query.
- [**`useResizeObserver`**](https://react-hookz.github.io/web/?path=/docs/sensor-useresizeobserver--example)
— Invokes a callback whenever ResizeObserver detects a change to target's size.
- [**`useScreenOrientation`**](https://react-hookz.github.io/web/?path=/docs/sensor-usescreenorientation--example)
— Checks if screen is in `portrait` or `landscape` orientation and automatically re-renders on
orientation change.

- #### Dom

Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export {
IUseKeyboardEventOptions,
} from './useKeyboardEvent/useKeyboardEvent';

export {
ScreenOrientation,
useScreenOrientation,
} from './useScreenOrientation/useScreenOrientation';

// Dom
export { useClickOutside } from './useClickOutside/useClickOutside';
export { useDocumentTitle, IUseDocumentTitleOptions } from './useDocumentTitle/useDocumentTitle';
Expand Down
1 change: 1 addition & 0 deletions src/usePreviousDistinct/usePreviousDistinct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
import { useUpdateEffect } from '..';
import { isStrictEqual } from '../util/const';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Predicate = (prev: any, next: any) => boolean;

/**
Expand Down
17 changes: 17 additions & 0 deletions src/useScreenOrientation/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { useScreenOrientation } from '../..';

export const Example: React.FC = () => {
const orientation = useScreenOrientation();

return (
<div>
<div>
Orientation: <code>{orientation}</code>
</div>
<div>
Render time: <code>{new Date().toLocaleString()}</code>
</div>
</div>
);
};
41 changes: 41 additions & 0 deletions src/useScreenOrientation/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './example.stories';
import { ImportPath } from '../../storybookUtil/ImportPath';

<Meta title="Sensor/useScreenOrientation" component={Example} />

# useScreenOrientation

Checks if screen is in `portrait` or `landscape` orientation and automatically re-renders on
orientation change.

As [Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API#browser_compatibility)
is still experimental and not supported by Safari, this hook uses CSS3 `orientation` media-query to
check screen orientation.

- Automatically re-renders on screen orientation change.
- Works in sync with CSS, as it uses media-query.
- All hooks uses single media query - therefore it is extremely performant.
- SSR-friendly.

#### Example

<Canvas>
<Story story={Example} inline />
</Canvas>

## Reference

```ts
type ScreenOrientation = 'portrait' | 'landscape';

function useScreenOrientation(): ScreenOrientation;
```

#### Importing

<ImportPath />

#### Return

A string representing screen orientation
71 changes: 71 additions & 0 deletions src/useScreenOrientation/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { act, renderHook } from '@testing-library/react-hooks/dom';
import { useScreenOrientation } from '../..';

describe('useScreenOrientation', () => {
// have to copy implementation as jsdom lacks of it
type IMutableMediaQueryList = {
matches: boolean;
media: string;
onchange: null;
addListener: jest.Mock; // Deprecated
removeListener: jest.Mock; // Deprecated
addEventListener: jest.Mock;
removeEventListener: jest.Mock;
dispatchEvent: jest.Mock;
};

const matchMediaMock = jest.fn();
let initialMatchMedia: typeof window.matchMedia;

beforeAll(() => {
initialMatchMedia = window.matchMedia;
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: matchMediaMock,
});
});

afterAll(() => {
window.matchMedia = initialMatchMedia;
});

beforeEach(() => {
matchMediaMock.mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
});

afterEach(() => {
matchMediaMock.mockClear();
});

it('should be defined', () => {
expect(useScreenOrientation).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useScreenOrientation());
expect(result.error).toBeUndefined();
});

it('should return `portrait` in case media query matches and `landscape` otherwise', () => {
const { result } = renderHook(() => useScreenOrientation());
expect(result.current).toBe('landscape');

const mql = matchMediaMock.mock.results[0].value as IMutableMediaQueryList;
mql.matches = true;

act(() => {
mql.addEventListener.mock.calls[0][1]();
});

expect(result.current).toBe('portrait');
});
});
13 changes: 13 additions & 0 deletions src/useScreenOrientation/__tests__/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useScreenOrientation } from '../..';

describe('useScreenOrientation', () => {
it('should be defined', () => {
expect(useScreenOrientation).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useScreenOrientation());
expect(result.error).toBeUndefined();
});
});
13 changes: 13 additions & 0 deletions src/useScreenOrientation/useScreenOrientation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useMediaQuery } from '..';

export type ScreenOrientation = 'portrait' | 'landscape';

/**
* Checks if screen is in `portrait` or `landscape` orientation.
*
* As `Screen Orientation API` is still experimental and not supported by Safari, this
* hook uses CSS3 `orientation` media-query to check screen orientation.
*/
export function useScreenOrientation(): ScreenOrientation {
return useMediaQuery('(orientation: portrait)') ? 'portrait' : 'landscape';
}
1 change: 1 addition & 0 deletions src/util/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const isBrowser =
* You should only be reaching for this function when you're attempting to prevent multiple
* redefinitions of the same function. In-place strict equality checks are more performant.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isStrictEqual: Predicate = (prev: any, next: any): boolean => prev === next;

export const truthyAndArrayPredicate: IConditionsPredicate = (conditions): boolean =>
Expand Down

0 comments on commit 107cc21

Please sign in to comment.