Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export * from './logo';
export { default as Media } from './media';
export * from './media';

export * from './media-query';

export * from './menu';

export * from './nav-sidebar';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
// @flow
import * as React from 'react';
import { mount } from 'enzyme';

import useMediaQuery from '../useMediaQuery';
import type { MediaShape } from '../types';

const WIDTH = 999;
const HEIGHT = 998;

type Props = {
children?: React.node,
};
interface FakeComponentProps {
children: (mediaProps: MediaShape) => React.ReactNode;
}

function FakeComponent(props: Props) {
function FakeComponent(props: FakeComponentProps) {
const mediaProps = useMediaQuery();

return <div>{props.children(mediaProps)}</div>;
}

function setWindowProperty(prop, value) {
function setWindowProperty(prop: 'innerHeight' | 'innerWidth', value: number) {
Object.defineProperty(window, prop, {
writable: true,
value,
Expand All @@ -31,7 +31,7 @@ describe('components/media-query/useMediaQuery', () => {

const mountedComponent = mount(
<FakeComponent>
{mediaProps => {
{(mediaProps: MediaShape) => {
return (
<div>
<div className="height">{mediaProps.viewHeight}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import withMediaQuery from '../withMediaQuery';
import type { MediaShape } from '../types';

describe('elements/common/media-query/withMediaQuery', () => {
const WrappedComponent = () => <div />;
const WrappedComponent = (props: MediaShape) => <div data-size={props.size} />;
const WithMediaComponent = withMediaQuery(WrappedComponent);

const getWrapper = props => shallow(<WithMediaComponent {...props} />);
const getWrapper = (props: React.ComponentProps<typeof WithMediaComponent>) =>
shallow(<WithMediaComponent {...props} />);

test('wraps component with media query props', () => {
const container = mount(<WithMediaComponent />);

const containerProps = container.find('WrappedComponent').props();
const containerProps = container.find(WrappedComponent).props();

expect(containerProps.size).not.toBeNull();
expect(containerProps.pointer).not.toBeNull();
Expand Down
32 changes: 32 additions & 0 deletions src/components/media-query/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { smallScreen, mediumScreen, largeScreen } from '../../styles/variables';

export const SIZE_SMALL = smallScreen;
export const SIZE_MEDIUM = mediumScreen;
export const SIZE_LARGE = largeScreen;

export const HOVER = '(hover: hover)';
export const ANY_HOVER = '(any-hover: hover)';

export const POINTER_FINE = '(pointer: fine)';
export const POINTER_COARSE = '(pointer: coarse)';

export const ANY_POINTER_FINE = '(any-pointer: fine)';
export const ANY_POINTER_COARSE = '(any-pointer: coarse)';

export const POINTER_TYPE = {
fine: 'fine',
coarse: 'coarse',
none: 'none',
} as const;

export const HOVER_TYPE = {
hover: 'hover',
none: 'none',
} as const;

export const VIEW_SIZE_TYPE = {
small: 'small',
medium: 'medium',
large: 'large',
xlarge: 'x-large',
} as const;
3 changes: 3 additions & 0 deletions src/components/media-query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as useMediaQuery } from './useMediaQuery';
export { default as withMediaQuery } from './withMediaQuery';
export type { MediaFeatures, MediaHoverType, MediaPointerType, MediaQuery, MediaShape } from './types';
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// @flow

import * as React from 'react';

import { VIEW_SIZE_TYPE } from '../constants';
import notes from './MediaQuery.stories.md';
import useMediaQuery from '../useMediaQuery';
import withMediaQuery from '../withMediaQuery';
import type { MediaShape } from '../types';

export const CustomHook = () => {
const { hover, isTouchDevice, pointer, size, viewWidth, viewHeight } = useMediaQuery();
Expand Down Expand Up @@ -36,17 +35,12 @@ export const CustomHook = () => {
);
};

type Props = {
children?: any,
hover: string,
isTouchDevice: boolean,
pointer: string,
size: string,
viewHeight: number,
viewWidth: number,
};
interface DemoComponentProps
extends Pick<MediaShape, 'hover' | 'isTouchDevice' | 'pointer' | 'size' | 'viewHeight' | 'viewWidth'> {
children?: React.ReactNode;
}

const DemoComponent = (props: Props) => {
const DemoComponent = (props: DemoComponentProps) => {
const { hover, isTouchDevice, pointer, size, viewWidth, viewHeight } = props;

return (
Expand Down
28 changes: 28 additions & 0 deletions src/components/media-query/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HOVER_TYPE, POINTER_TYPE } from './constants';

export type MediaPointerType = (typeof POINTER_TYPE)[keyof typeof POINTER_TYPE];
export type MediaHoverType = (typeof HOVER_TYPE)[keyof typeof HOVER_TYPE];

export interface MediaFeatures {
anyHover?: MediaHoverType;
anyPointer?: MediaPointerType;
hover?: MediaHoverType;
maxHeight?: number;
maxWidth?: number;
minHeight?: number;
minWidth?: number;
pointer?: MediaPointerType;
}

export interface MediaShape {
anyHover: MediaHoverType;
anyPointer: MediaPointerType;
hover: MediaHoverType;
isTouchDevice: boolean;
pointer: MediaPointerType;
size: string;
viewHeight: number;
viewWidth: number;
}

export type MediaQuery = string | MediaFeatures;
98 changes: 98 additions & 0 deletions src/components/media-query/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { toQuery, useMediaQuery as _useMediaQuery } from 'react-responsive';
import {
ANY_HOVER,
ANY_POINTER_COARSE,
ANY_POINTER_FINE,
HOVER,
HOVER_TYPE,
POINTER_COARSE,
POINTER_FINE,
POINTER_TYPE,
SIZE_LARGE,
SIZE_MEDIUM,
SIZE_SMALL,
VIEW_SIZE_TYPE,
} from './constants';
import type { MediaQuery, MediaShape } from './types';

const getPointerCapabilities = (isFine: boolean, isCoarse: boolean) => {
if (!isFine && !isCoarse) return POINTER_TYPE.none;
if (isFine) return POINTER_TYPE.fine;
return POINTER_TYPE.coarse;
};

const getViewDimensions = () => {
return { viewWidth: window.innerWidth, viewHeight: window.innerHeight };
};
Comment thread
bonchevskyi marked this conversation as resolved.

/**
* Formats the media query either as a MediaQuery object or string
* @param query
* @returns {string}
*/
function formatQuery(query: MediaQuery): string {
Comment thread
bonchevskyi marked this conversation as resolved.
return typeof query === 'string' ? query : toQuery(query);
}

/**
* Executes media query
* @param query
* @param onQueryChange
* @returns {boolean}
*/
function useQuery(query: MediaQuery, onQueryChange?: (_: boolean) => void): boolean {
return _useMediaQuery({ query: formatQuery(query) }, null, onQueryChange);
}

/**
* Determines device capabilities for hover and pointer features
* @returns {{anyPointer: *, hover: (string), pointer: *, anyHover: (string)}}
*/
function useDeviceCapabilities() {
const isHover: boolean = useQuery(HOVER);
const isAnyHover: boolean = useQuery(ANY_HOVER);

const anyHover = isAnyHover ? HOVER_TYPE.hover : HOVER_TYPE.none;
const hover = isHover ? HOVER_TYPE.hover : HOVER_TYPE.none;
const pointer = getPointerCapabilities(useQuery(POINTER_FINE), useQuery(POINTER_COARSE));
const anyPointer = getPointerCapabilities(useQuery(ANY_POINTER_FINE), useQuery(ANY_POINTER_COARSE));

const isTouchDevice = hover === HOVER_TYPE.none && pointer === POINTER_TYPE.coarse;

return {
anyHover,
hover,
anyPointer,
pointer,
isTouchDevice,
};
}

/**
* Determines device size using media queries
* @returns {string}
*/
function useDeviceSize() {
const isSmall: boolean = useQuery(SIZE_SMALL);
const isMedium: boolean = useQuery(SIZE_MEDIUM);
const isLarge: boolean = useQuery(SIZE_LARGE);

if (isSmall) return VIEW_SIZE_TYPE.small;
if (isMedium) return VIEW_SIZE_TYPE.medium;
if (isLarge) return VIEW_SIZE_TYPE.large;

return VIEW_SIZE_TYPE.xlarge;
}

function useMediaQuery(): MediaShape {
const deviceCapabilities = useDeviceCapabilities();
const deviceSize = useDeviceSize();
const viewDimensions = getViewDimensions();
return {
...deviceCapabilities,
...viewDimensions,
size: deviceSize,
};
}

export default useMediaQuery;
15 changes: 15 additions & 0 deletions src/components/media-query/withMediaQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import useMediaQuery from './useMediaQuery';
import type { MediaShape } from './types';

function withMediaQuery<Props extends object>(
Comment thread
bonchevskyi marked this conversation as resolved.
WrappedComponent: React.ComponentType<Props>,
): React.ComponentType<Record<string, unknown>> {
return (props: Record<string, unknown>) => {
const mediaProps: MediaShape = useMediaQuery();

return <WrappedComponent {...({ ...props, ...mediaProps } as Props)} />;
};
Comment thread
bonchevskyi marked this conversation as resolved.
}

export default withMediaQuery;
Loading