diff --git a/src/components/index.ts b/src/components/index.ts index f3432cca33..0186feb291 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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'; diff --git a/src/components/media-query/__tests__/useMediaQuery.test.js b/src/components/media-query/__tests__/useMediaQuery.test.tsx similarity index 78% rename from src/components/media-query/__tests__/useMediaQuery.test.js rename to src/components/media-query/__tests__/useMediaQuery.test.tsx index eb60b3807b..c14c67fbf5 100644 --- a/src/components/media-query/__tests__/useMediaQuery.test.js +++ b/src/components/media-query/__tests__/useMediaQuery.test.tsx @@ -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
{props.children(mediaProps)}
; } -function setWindowProperty(prop, value) { +function setWindowProperty(prop: 'innerHeight' | 'innerWidth', value: number) { Object.defineProperty(window, prop, { writable: true, value, @@ -31,7 +31,7 @@ describe('components/media-query/useMediaQuery', () => { const mountedComponent = mount( - {mediaProps => { + {(mediaProps: MediaShape) => { return (
{mediaProps.viewHeight}
diff --git a/src/components/media-query/__tests__/withMediaQuery.test.js b/src/components/media-query/__tests__/withMediaQuery.test.tsx similarity index 73% rename from src/components/media-query/__tests__/withMediaQuery.test.js rename to src/components/media-query/__tests__/withMediaQuery.test.tsx index a9ce4739ef..6499d9f049 100644 --- a/src/components/media-query/__tests__/withMediaQuery.test.js +++ b/src/components/media-query/__tests__/withMediaQuery.test.tsx @@ -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 = () =>
; + const WrappedComponent = (props: MediaShape) =>
; const WithMediaComponent = withMediaQuery(WrappedComponent); - const getWrapper = props => shallow(); + const getWrapper = (props: React.ComponentProps) => + shallow(); test('wraps component with media query props', () => { const container = mount(); - const containerProps = container.find('WrappedComponent').props(); + const containerProps = container.find(WrappedComponent).props(); expect(containerProps.size).not.toBeNull(); expect(containerProps.pointer).not.toBeNull(); diff --git a/src/components/media-query/constants.js b/src/components/media-query/constants.js.flow similarity index 100% rename from src/components/media-query/constants.js rename to src/components/media-query/constants.js.flow diff --git a/src/components/media-query/constants.ts b/src/components/media-query/constants.ts new file mode 100644 index 0000000000..a205166a2b --- /dev/null +++ b/src/components/media-query/constants.ts @@ -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; diff --git a/src/components/media-query/index.js b/src/components/media-query/index.js.flow similarity index 100% rename from src/components/media-query/index.js rename to src/components/media-query/index.js.flow diff --git a/src/components/media-query/index.ts b/src/components/media-query/index.ts new file mode 100644 index 0000000000..97bd650ef9 --- /dev/null +++ b/src/components/media-query/index.ts @@ -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'; diff --git a/src/components/media-query/stories/MediaQuery.stories.js b/src/components/media-query/stories/MediaQuery.stories.tsx similarity index 89% rename from src/components/media-query/stories/MediaQuery.stories.js rename to src/components/media-query/stories/MediaQuery.stories.tsx index 900875bb56..3717c18919 100644 --- a/src/components/media-query/stories/MediaQuery.stories.js +++ b/src/components/media-query/stories/MediaQuery.stories.tsx @@ -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(); @@ -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 { + children?: React.ReactNode; +} -const DemoComponent = (props: Props) => { +const DemoComponent = (props: DemoComponentProps) => { const { hover, isTouchDevice, pointer, size, viewWidth, viewHeight } = props; return ( diff --git a/src/components/media-query/types.js b/src/components/media-query/types.js.flow similarity index 100% rename from src/components/media-query/types.js rename to src/components/media-query/types.js.flow diff --git a/src/components/media-query/types.ts b/src/components/media-query/types.ts new file mode 100644 index 0000000000..a00d16c280 --- /dev/null +++ b/src/components/media-query/types.ts @@ -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; diff --git a/src/components/media-query/useMediaQuery.js b/src/components/media-query/useMediaQuery.js.flow similarity index 100% rename from src/components/media-query/useMediaQuery.js rename to src/components/media-query/useMediaQuery.js.flow diff --git a/src/components/media-query/useMediaQuery.ts b/src/components/media-query/useMediaQuery.ts new file mode 100644 index 0000000000..ddbf01d78f --- /dev/null +++ b/src/components/media-query/useMediaQuery.ts @@ -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 }; +}; + +/** + * Formats the media query either as a MediaQuery object or string + * @param query + * @returns {string} + */ +function formatQuery(query: MediaQuery): string { + 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; diff --git a/src/components/media-query/withMediaQuery.js b/src/components/media-query/withMediaQuery.js.flow similarity index 100% rename from src/components/media-query/withMediaQuery.js rename to src/components/media-query/withMediaQuery.js.flow diff --git a/src/components/media-query/withMediaQuery.tsx b/src/components/media-query/withMediaQuery.tsx new file mode 100644 index 0000000000..fa222a962c --- /dev/null +++ b/src/components/media-query/withMediaQuery.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import useMediaQuery from './useMediaQuery'; +import type { MediaShape } from './types'; + +function withMediaQuery( + WrappedComponent: React.ComponentType, +): React.ComponentType> { + return (props: Record) => { + const mediaProps: MediaShape = useMediaQuery(); + + return ; + }; +} + +export default withMediaQuery;